@nano-app/plugins 0.2.0 → 0.3.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/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # @nano-app/plugins
2
2
 
3
+ [![plugins](https://img.shields.io/npm/v/@nano-app/plugins?label=plugins)](https://www.npmjs.com/package/@nano-app/plugins)
4
+
3
5
  Official plugins for [`@nano-app/backend`](https://www.npmjs.com/package/@nano-app/backend). One import path per plugin: `@nano-app/plugins/<name>`. Nothing is mounted unless you list it.
4
6
 
5
7
  ```bash
6
- npm install @nano-app/backend @nano-app/plugins
8
+ pnpm add @nano-app/backend @nano-app/plugins
7
9
  ```
8
10
 
9
11
  ```ts
10
- import { createNanoApp } from "@nano-app/backend";
11
- import health from "@nano-app/plugins/health";
12
+ import { createNanoApp } from '@nano-app/backend';
13
+ import health from '@nano-app/plugins/health';
12
14
 
13
15
  createNanoApp({ plugins: [health()] });
14
16
  ```
@@ -17,62 +19,7 @@ createNanoApp({ plugins: [health()] });
17
19
 
18
20
  | Plugin | Import | Endpoints | Options |
19
21
  |---|---|---|---|
20
- | `health` | `import health from "@nano-app/plugins/health"` | `GET /health` → `{ status: "ok" }` | `basePath` (default `"/health"`) |
21
- | `auth` | `import auth from "@nano-app/plugins/auth"` | `/api-keys`, and an API key guard on every route after it | see [auth](#auth) |
22
-
23
- ## auth
24
-
25
- API keys. Every route mounted after the plugin needs a valid key in the `x-api-key` header; the routes of the plugins listed before it and `publicPaths` stay public. The keys are managed with the master key, created on the first startup.
26
-
27
- ```ts
28
- import { createNanoApp } from "@nano-app/backend";
29
- import health from "@nano-app/plugins/health";
30
- import auth from "@nano-app/plugins/auth";
31
-
32
- // health is listed before auth: it stays public
33
- createNanoApp({ plugins: [health(), auth({ publicPaths: ["/webhooks/telegram"] })] });
34
- ```
35
-
36
- | Option | Default | |
37
- |---|---|---|
38
- | `basePath` | `"/api-keys"` | Base path of the endpoints that manage the keys |
39
- | `publicPaths` | `[]` | Exact paths that need no key |
40
-
41
- ### Table
42
-
43
- The keys live in `nano_auth.api_key`, through the app's Prisma (`DATABASE_URL`). Only the SHA-256 of each key is stored. nano-app creates and migrates the table at startup: there is nothing to run.
44
-
45
- ### Master key
46
-
47
- The first startup creates it and prints it **once** in the console: keep it. A unique index allows a single master, also when several instances start together. It cannot be deleted. If it is lost, delete its row (`isMaster = true`) and restart: a new one is created and printed.
48
-
49
- ### Endpoints
50
-
51
- Only the master key reaches them (403 with any other key).
52
-
53
- | Request | Answer |
54
- |---|---|
55
- | `GET /api-keys` | The keys: `id`, `name`, `isMaster`, `createdAt`. Never the key |
56
- | `POST /api-keys` `{ name }` | A new key, with its `key` only in this response |
57
- | `DELETE /api-keys/:id` | Deletes it; it stops working at once. 403 on the master |
58
- | `POST /api-keys/:id/renew` | Same id, a new `key`; the old one stops working at once |
59
- | `POST /api-keys/master/renew` | Renews the master |
60
-
61
- ### Guard
62
-
63
- | Request | Answer |
64
- |---|---|
65
- | Path in `publicPaths`, or served before the plugin | passes |
66
- | No key, or a key that does not exist | 401 |
67
- | Any valid key, the master included | passes |
68
-
69
- ### Users and sessions
70
-
71
- The plugin covers API keys only: it has no users, login or sessions. If your app needs them, we recommend integrating [Better Auth](https://www.better-auth.com) yourself:
72
-
73
- - Wrap its handler in a plugin of your own and list it **before** `auth()`, so its endpoints (sign-in, sign-up, ...) stay public.
74
- - Give its tables their own PostgreSQL schema, outside `nano_*`, and apply its migrations with its CLI: nano-app does not migrate them.
75
- - To protect your routes with a session instead of a key, check it in your handlers or in an `onBefore` of your own.
22
+ | `health` | `import health from '@nano-app/plugins/health'` | `GET /health` → `{ status: "ok" }` | `basePath` (default `"/health"`) |
76
23
 
77
24
  ## Layout
78
25
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nano-app/plugins",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Official plugins for @nano-app/backend",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,9 +0,0 @@
1
- import { AuthOptions } from "../types/auth.js";
2
- //#region auth/index.d.ts
3
- /**
4
- * API keys: every route mounted after this plugin needs a valid `x-api-key`.
5
- * The master key, created on the first startup, manages the rest under `basePath`
6
- */
7
- declare const _default: (options?: AuthOptions | undefined) => import("@nano-app/backend").PluginConfig<AuthOptions>;
8
- //#endregion
9
- export { _default as default };
@@ -1,36 +0,0 @@
1
- import { ensureMaster, findKey } from "./keys.js";
2
- import { dirname, resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { createPlugin, createPluginCallbacks } from "@nano-app/backend";
5
- //#region auth/index.ts
6
- /** Folder of this file, from the sources and from the build */
7
- const DIR = dirname(fileURLToPath(import.meta.url));
8
- /**
9
- * API keys: every route mounted after this plugin needs a valid `x-api-key`.
10
- * The master key, created on the first startup, manages the rest under `basePath`
11
- */
12
- var auth_default = createPlugin((options) => {
13
- const { basePath = "/api-keys", publicPaths = [] } = options ?? {};
14
- return {
15
- name: "auth",
16
- basePath,
17
- stage: "before",
18
- routesDir: resolve(DIR, "routes"),
19
- database: {
20
- enabled: true,
21
- schemaPath: resolve(DIR, "schema.prisma")
22
- },
23
- callbacks: createPluginCallbacks({ onMount: async (oApp, { db }) => {
24
- await ensureMaster(db);
25
- oApp.use((oReq, _oRes, fnNext) => {
26
- if (publicPaths.includes(oReq.path)) return fnNext();
27
- findKey(db, oReq.get("x-api-key")).then((oKey) => fnNext(oKey ? void 0 : {
28
- status: 401,
29
- message: "Invalid or missing API key"
30
- })).catch(fnNext);
31
- });
32
- } })
33
- };
34
- });
35
- //#endregion
36
- export { auth_default as default };
@@ -1,36 +0,0 @@
1
- import { createHash, randomBytes } from "node:crypto";
2
- //#region auth/keys.ts
3
- /** Name of the master key */
4
- const MASTER = "master";
5
- /** A new random key: 32 bytes, URL-safe */
6
- function generateKey() {
7
- return randomBytes(32).toString("base64url");
8
- }
9
- /** SHA-256 of a key, as the table stores it */
10
- function hashKey(sKey) {
11
- return createHash("sha256").update(sKey).digest("hex");
12
- }
13
- /** The stored key that matches the `x-api-key` header, or null */
14
- async function findKey(oDb, sKey) {
15
- if (!sKey) return null;
16
- return oDb.apiKey.findUnique({ where: { hash: hashKey(sKey) } });
17
- }
18
- /**
19
- * Creates the master key when there is none, and prints it once.
20
- * The unique index on isMaster lets a single instance insert it when several start together
21
- */
22
- async function ensureMaster(oDb) {
23
- const sKey = generateKey();
24
- const { count } = await oDb.apiKey.createMany({
25
- data: [{
26
- name: MASTER,
27
- hash: hashKey(sKey),
28
- isMaster: true
29
- }],
30
- skipDuplicates: true
31
- });
32
- if (!count) return;
33
- console.warn(["[nano-app] [WARNING] auth: master API key created. It is shown only once, keep it:", sKey].join("\n"));
34
- }
35
- //#endregion
36
- export { MASTER, ensureMaster, findKey, generateKey, hashKey };
@@ -1,20 +0,0 @@
1
- -- CreateSchema
2
- CREATE SCHEMA IF NOT EXISTS "nano_auth";
3
-
4
- -- CreateTable
5
- CREATE TABLE "nano_auth"."api_key" (
6
- "id" TEXT NOT NULL,
7
- "name" TEXT NOT NULL,
8
- "hash" TEXT NOT NULL,
9
- "isMaster" BOOLEAN,
10
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11
-
12
- CONSTRAINT "api_key_pkey" PRIMARY KEY ("id")
13
- );
14
-
15
- -- CreateIndex
16
- CREATE UNIQUE INDEX "api_key_hash_key" ON "nano_auth"."api_key"("hash");
17
-
18
- -- CreateIndex
19
- CREATE UNIQUE INDEX "api_key_isMaster_key" ON "nano_auth"."api_key"("isMaster");
20
-
@@ -1,3 +0,0 @@
1
- # Please do not edit this file manually
2
- # It should be added in your version-control system (e.g., Git)
3
- provider = "postgresql"
@@ -1,96 +0,0 @@
1
- import { findKey, generateKey, hashKey } from "../keys.js";
2
- import { createRoute } from "@nano-app/backend";
3
- //#region auth/routes/index.route.ts
4
- /** Columns the endpoints return: never the hash */
5
- const SELECT = {
6
- id: true,
7
- name: true,
8
- isMaster: true,
9
- createdAt: true
10
- };
11
- /** 404 of an id or an action that does not exist */
12
- function notFound(oReq) {
13
- return {
14
- status: 404,
15
- message: `${oReq.path} not found`
16
- };
17
- }
18
- /**
19
- * API keys, managed only with the master key:
20
- * GET / list them
21
- * POST / create one { name }; the key is returned only now
22
- * DELETE /:id delete one (not the master)
23
- * POST /:id/renew new key, same id ("master" renews the master)
24
- */
25
- var index_route_default = createRoute({
26
- expression: "{/:id}{/:action}",
27
- callbacks: { onBefore: async (oReq, _sMethod, { db }) => {
28
- if (!(await findKey(db, oReq.get("x-api-key")))?.isMaster) throw {
29
- status: 403,
30
- message: "Only the master API key manages API keys"
31
- };
32
- return {};
33
- } },
34
- GET: async (oReq, { db }) => {
35
- if (oReq.params.id) throw notFound(oReq);
36
- return {
37
- success: true,
38
- result: await db.apiKey.findMany({ select: SELECT })
39
- };
40
- },
41
- POST: async (oReq, { db }) => {
42
- const { id, action } = oReq.params;
43
- if (id && action === "renew") {
44
- const oWhere = id === "master" ? { isMaster: true } : { id };
45
- const oKey = await db.apiKey.findFirst({ where: oWhere });
46
- if (!oKey) throw notFound(oReq);
47
- const sKey = generateKey();
48
- await db.apiKey.update({
49
- where: { id: oKey.id },
50
- data: { hash: hashKey(sKey) }
51
- });
52
- return {
53
- success: true,
54
- result: {
55
- id: oKey.id,
56
- name: oKey.name,
57
- key: sKey
58
- }
59
- };
60
- }
61
- if (id) throw notFound(oReq);
62
- const { name } = oReq.body ?? {};
63
- if (typeof name !== "string" || !name.trim()) throw "The API key needs a name";
64
- const sKey = generateKey();
65
- return {
66
- success: true,
67
- result: {
68
- ...await db.apiKey.create({
69
- data: {
70
- name,
71
- hash: hashKey(sKey)
72
- },
73
- select: SELECT
74
- }),
75
- key: sKey
76
- }
77
- };
78
- },
79
- DELETE: async (oReq, { db }) => {
80
- const { id, action } = oReq.params;
81
- if (!id || action) throw notFound(oReq);
82
- const oKey = await db.apiKey.findUnique({ where: { id } });
83
- if (!oKey) throw notFound(oReq);
84
- if (oKey.isMaster) throw {
85
- status: 403,
86
- message: "The master API key cannot be deleted"
87
- };
88
- await db.apiKey.delete({ where: { id } });
89
- return {
90
- success: true,
91
- result: { id }
92
- };
93
- }
94
- });
95
- //#endregion
96
- export { index_route_default as default };
@@ -1,23 +0,0 @@
1
- // API keys of the auth plugin, in the nano_auth schema
2
- datasource db {
3
- provider = "postgresql"
4
- schemas = ["nano_auth"]
5
- }
6
-
7
- generator client {
8
- provider = "prisma-client"
9
- output = "./generated"
10
- }
11
-
12
- model ApiKey {
13
- id String @id @default(uuid())
14
- name String
15
- /// SHA-256 of the key: the key itself is never stored
16
- hash String @unique
17
- /// true on the master key, null on the rest: the unique index allows a single master
18
- isMaster Boolean? @unique
19
- createdAt DateTime @default(now())
20
-
21
- @@map("api_key")
22
- @@schema("nano_auth")
23
- }
@@ -1,9 +0,0 @@
1
- //#region types/auth.d.ts
2
- /** Options of the auth plugin */
3
- export type AuthOptions = {
4
- /** Base path of the API key endpoints. Defaults to "/api-keys" */
5
- basePath?: string;
6
- /** Exact paths that need no API key, e.g. "/webhooks/telegram". Defaults to [] */
7
- publicPaths?: string[];
8
- };
9
- //#endregion