notifkit 0.1.4 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notifkit",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Self-hosted notification infrastructure. One call delivers to email, SMS, push, and webhook — routed by preference, quiet hours, and consent.",
5
5
  "license": "MIT",
6
6
  "author": "devkitshq",
@@ -48,12 +48,17 @@
48
48
  "dist",
49
49
  "src",
50
50
  "drizzle",
51
+ "scripts/create-project.mjs",
51
52
  "README.md",
52
53
  "LICENSE"
53
54
  ],
55
+ "bin": {
56
+ "notifkit-create-project": "./scripts/create-project.mjs"
57
+ },
54
58
  "scripts": {
55
59
  "build": "tsdown",
56
60
  "dev": "tsdown --watch",
61
+ "create-project": "node scripts/create-project.mjs",
57
62
  "test": "vitest run --coverage",
58
63
  "test:watch": "vitest",
59
64
  "lint": "eslint . --fix",
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Create a project and print its API key.
4
+ *
5
+ * Every `/v1/*` route needs a project API key, and a project API key can only
6
+ * be minted with the admin credential, so this is the one bootstrap step
7
+ * between a running server and the first notification.
8
+ *
9
+ * ADMIN_API_KEY=supersecretkey npm run create-project
10
+ * ADMIN_API_KEY=supersecretkey npx notifkit-create-project "my-app"
11
+ *
12
+ * ADMIN_API_KEY is the same variable the server reads, so pass the value the
13
+ * server was started with.
14
+ */
15
+
16
+ const adminKey = process.env.ADMIN_API_KEY;
17
+ const baseUrl = (process.env.NOTIFKIT_URL || "http://localhost:3000").replace(/\/$/, "");
18
+ const name = process.argv[2] || process.env.PROJECT_NAME || "my-app";
19
+
20
+ function fail(message) {
21
+ console.error(`create-project: ${message}`);
22
+ process.exit(1);
23
+ }
24
+
25
+ if (!adminKey) {
26
+ fail(
27
+ "no admin credential. Set ADMIN_API_KEY to the same value the server was started with:\n" +
28
+ " ADMIN_API_KEY=supersecretkey npm run create-project",
29
+ );
30
+ }
31
+
32
+ let res;
33
+ try {
34
+ res = await fetch(`${baseUrl}/v1/projects`, {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${adminKey}` },
37
+ body: JSON.stringify({ name }),
38
+ });
39
+ } catch (err) {
40
+ fail(`could not reach the server at ${baseUrl} — is it running? (${err.message})`);
41
+ }
42
+
43
+ if (res.status === 401) {
44
+ fail("admin credential rejected. It must match the server's ADMIN_API_KEY exactly.");
45
+ }
46
+
47
+ if (res.status === 403) {
48
+ fail("project management is disabled. Start the server with ADMIN_API_KEY set.");
49
+ }
50
+
51
+ if (!res.ok) {
52
+ fail(`server returned ${res.status}: ${await res.text()}`);
53
+ }
54
+
55
+ const { id, apiKey } = await res.json();
56
+
57
+ // The server keeps only a hash of the key, so this is the only time the value
58
+ // itself exists anywhere. Print it in .env form to make that the obvious move.
59
+ console.log(`\nProject "${name}" created. Save the API key now — it is not recoverable.\n`);
60
+ console.log(`NOTIFKIT_PROJECT_ID=${id}`);
61
+ console.log(`NOTIFKIT_API_KEY=${apiKey}\n`);
@@ -38,7 +38,11 @@ export const baseConfigSchema = z.object({
38
38
  HOST: z.string().default("127.0.0.1"),
39
39
  REDIS_URL: z.string().url().default("redis://localhost:6379"),
40
40
  DATABASE_URL: z.string().url().default("postgres://platform:platform@localhost:5432/notifkit"),
41
- ADMIN_API_KEY: z.string().optional(),
41
+ // Trimmed because the incoming bearer token is trimmed before comparison, so
42
+ // an untrimmed value here could never match it. `set KEY=value && cmd` on
43
+ // Windows puts a trailing space in the variable, which otherwise turns every
44
+ // admin request into a 401 that reads like a wrong key.
45
+ ADMIN_API_KEY: z.string().trim().optional(),
42
46
  WORKER_CONCURRENCY: z.coerce.number().int().min(1).default(10),
43
47
  QUEUE_MAX_LEN: z.coerce.number().int().min(1).default(10000000),
44
48
  DB_MAX_CONNECTIONS: z.coerce.number().int().min(1).default(2),