nucleus-core-ts 0.10.141 → 0.10.143

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 (169) hide show
  1. package/README.md +177 -177
  2. package/bin/cli.ts +136 -136
  3. package/bin/lib/commands/add.ts +248 -248
  4. package/bin/lib/commands/dev.ts +213 -213
  5. package/bin/lib/commands/doctor.ts +91 -91
  6. package/bin/lib/commands/init.ts +147 -147
  7. package/bin/lib/commands/validate.ts +137 -137
  8. package/bin/lib/config-builder.ts +84 -84
  9. package/bin/lib/env.ts +85 -85
  10. package/bin/lib/presets.ts +124 -124
  11. package/bin/lib/schema.ts +132 -132
  12. package/bin/lib/templates.ts +154 -154
  13. package/bin/lib/ui.ts +126 -126
  14. package/dist/.build-ok +1 -1
  15. package/dist/client.js +11 -2
  16. package/dist/dist/index.d.ts +9 -1
  17. package/dist/fe/components/AuthGuard/store.js +11 -11
  18. package/dist/fe/components/VerificationFlowPage/components/PendingTab.d.ts +7 -2
  19. package/dist/fe/components/VerificationFlowPage/components/PendingTab.js +171 -118
  20. package/dist/fe/components/VerificationFlowPage/components/PropertiesPanel.js +6 -6
  21. package/dist/fe/components/VerificationFlowPage/components/VerificationEntityList.d.ts +22 -2
  22. package/dist/fe/components/VerificationFlowPage/components/VerificationEntityList.js +171 -67
  23. package/dist/fe/components/VerificationFlowPage/components/VerificationFlowPage.d.ts +1 -1
  24. package/dist/fe/components/VerificationFlowPage/components/VerificationFlowPage.js +307 -253
  25. package/dist/fe/components/VerificationFlowPage/components/VerificationStatusCard.js +19 -19
  26. package/dist/fe/components/VerificationFlowPage/components/decisionPlan.d.ts +20 -0
  27. package/dist/fe/components/VerificationFlowPage/components/decisionPlan.js +137 -98
  28. package/dist/fe/components/VerificationFlowPage/components/recordLabels.d.ts +41 -0
  29. package/dist/fe/components/VerificationFlowPage/components/recordLabels.js +58 -0
  30. package/dist/fe/components/VerificationFlowPage/components/supersededCard.d.ts +14 -0
  31. package/dist/fe/components/VerificationFlowPage/components/supersededCard.js +17 -0
  32. package/dist/fe/components/VerificationFlowPage/components/useEntityLabels.d.ts +12 -0
  33. package/dist/fe/components/VerificationFlowPage/components/useEntityLabels.js +40 -0
  34. package/dist/fe/components/VerificationFlowPage/index.d.ts +1 -1
  35. package/dist/fe/components/VerificationFlowPage/index.js +17 -17
  36. package/dist/fe/components/VerificationFlowPage/labels.d.ts +60 -0
  37. package/dist/fe/components/VerificationFlowPage/labels.js +56 -38
  38. package/dist/fe/components/VerificationFlowPage/model2/ActionRows.js +212 -212
  39. package/dist/fe/components/VerificationFlowPage/model2/AssigneeRows.js +37 -37
  40. package/dist/fe/components/VerificationFlowPage/model2/FlowDiagram.js +175 -175
  41. package/dist/fe/components/VerificationFlowPage/model2/OutcomeRows.js +49 -49
  42. package/dist/fe/components/VerificationFlowPage/model2/ReadinessCard.js +65 -65
  43. package/dist/fe/components/VerificationFlowPage/model2/StepCard.js +15 -15
  44. package/dist/fe/components/VerificationFlowPage/model2/StepListEditor.js +75 -75
  45. package/dist/fe/components/VerificationFlowPage/model2/editorGraph.js +190 -190
  46. package/dist/fe/components/VerificationFlowPage/model2/readinessText.js +4 -4
  47. package/dist/fe/components/VerificationFlowPage/model2/theme.js +17 -17
  48. package/dist/fe/components/VerificationFlowPage/stepOrder.js +19 -19
  49. package/dist/fe/components/VerificationFlowPage/store/index.js +76 -76
  50. package/dist/fe/components/VerificationFlowPage/theme/index.js +7 -7
  51. package/dist/fe/components/VerificationFlowPage/types/index.d.ts +40 -0
  52. package/dist/fe/components/VerificationFlowPage/types/index.js +7 -7
  53. package/dist/fe/components/VerificationPanel/components/PendingVerifications.js +30 -30
  54. package/dist/fe/components/VerificationPanel/components/VerificationPanel.js +25 -25
  55. package/dist/fe/index.d.ts +1 -1
  56. package/dist/fe/utils/newId.js +20 -20
  57. package/dist/index.d.ts +17 -0
  58. package/dist/index.js +34 -23
  59. package/dist/server.d.ts +4 -0
  60. package/dist/server.js +34 -23
  61. package/dist/src/Client/ApiCaller/types.d.ts +29 -0
  62. package/dist/src/Client/Proxy/httpProxy.js +5 -3
  63. package/dist/src/Client/Proxy/index.d.ts +1 -1
  64. package/dist/src/Client/Proxy/index.js +1 -1
  65. package/dist/src/Client/Proxy/synthesizedAuth.js +10 -10
  66. package/dist/src/Client/Proxy/types.d.ts +34 -0
  67. package/dist/src/Client/Proxy/utils.d.ts +21 -0
  68. package/dist/src/Client/Proxy/utils.js +61 -4
  69. package/dist/src/Client/Proxy/wsProxy.js +158 -16
  70. package/dist/src/Client/PubSub/usePubSub.js +97 -97
  71. package/dist/src/Client/PubSub/waitForReturn.js +29 -29
  72. package/dist/src/Client/Upload/progress.js +31 -31
  73. package/dist/src/Client/Upload/types.js +7 -7
  74. package/dist/src/Client/Upload/upload.js +21 -21
  75. package/dist/src/Nucleus/routes/auth/admin/impersonate.d.ts +9 -1
  76. package/dist/src/Nucleus/routes/auth/admin/impersonationSession.d.ts +191 -0
  77. package/dist/src/Nucleus/routes/auth/admin/impersonationStore.d.ts +108 -0
  78. package/dist/src/Nucleus/routes/auth/apiKeys/effectivePermissions.d.ts +43 -0
  79. package/dist/src/Nucleus/routes/auth/assignDefaultRole.d.ts +3 -19
  80. package/dist/src/Nucleus/routes/auth/cohort/handlers/activateUsers.d.ts +2 -2
  81. package/dist/src/Nucleus/routes/auth/cohort/handlers/bulkCreate.d.ts +2 -2
  82. package/dist/src/Nucleus/routes/auth/cohort/handlers/create.d.ts +2 -2
  83. package/dist/src/Nucleus/routes/auth/cohort/handlers/deactivateUsers.d.ts +2 -2
  84. package/dist/src/Nucleus/routes/auth/cohort/handlers/deleteUsers.d.ts +2 -2
  85. package/dist/src/Nucleus/routes/auth/cohort/handlers/detail.d.ts +2 -2
  86. package/dist/src/Nucleus/routes/auth/cohort/handlers/exportUsers.d.ts +2 -2
  87. package/dist/src/Nucleus/routes/auth/cohort/handlers/list.d.ts +2 -2
  88. package/dist/src/Nucleus/routes/auth/cohort/handlers/remove.d.ts +2 -2
  89. package/dist/src/Nucleus/routes/auth/cohort/handlers/update.d.ts +2 -2
  90. package/dist/src/Nucleus/routes/auth/cohort/types.d.ts +11 -0
  91. package/dist/src/Nucleus/routes/auth/cohort/utils.d.ts +3 -6
  92. package/dist/src/Nucleus/routes/auth/impersonationCookies.d.ts +18 -7
  93. package/dist/src/Nucleus/routes/auth/oauth/applyProfileSync.d.ts +1 -1
  94. package/dist/src/Nucleus/routes/auth/sessionPools.d.ts +102 -0
  95. package/dist/src/Nucleus/routes/auth/sessionRevocation.d.ts +75 -0
  96. package/dist/src/Nucleus/routes/auth/types.d.ts +5 -2
  97. package/dist/src/Nucleus/routes/entity/claimsCacheRefresh.d.ts +18 -0
  98. package/dist/src/Nucleus/routes/entity/godFlagSync.d.ts +50 -0
  99. package/dist/src/Nucleus/routes/payment/marketplace/types.d.ts +2 -1
  100. package/dist/src/Nucleus/routes/shared/adminGuard.d.ts +54 -3
  101. package/dist/src/Nucleus/routes/shared/apiKeyCeiling.d.ts +50 -0
  102. package/dist/src/Nucleus/routes/shared/godminGuard.d.ts +2 -1
  103. package/dist/src/Nucleus/routes/shared/redact.d.ts +5 -0
  104. package/dist/src/Nucleus/routes/shared/sessionOnly.d.ts +43 -0
  105. package/dist/src/Nucleus/routes/storage/cachePolicy.d.ts +37 -0
  106. package/dist/src/Nucleus/routes/storage/cdn.d.ts +21 -0
  107. package/dist/src/Nucleus/routes/storage/file-record.d.ts +12 -0
  108. package/dist/src/Nucleus/routes/storage/serveGuard.d.ts +60 -0
  109. package/dist/src/Services/Authorization/ClaimGuard/mostGenerous.d.ts +127 -0
  110. package/dist/src/Services/Authorization/ClaimGuard/resolve.d.ts +49 -6
  111. package/dist/src/Services/Authorization/ClaimGuard/types.d.ts +5 -2
  112. package/dist/src/Services/Authorization/EndpointDiscovery/fetch.d.ts +64 -0
  113. package/dist/src/Services/Authorization/EndpointDiscovery/hooks.d.ts +113 -0
  114. package/dist/src/Services/Authorization/EndpointDiscovery/index.d.ts +21 -2
  115. package/dist/src/Services/Authorization/EndpointDiscovery/seed.d.ts +28 -25
  116. package/dist/src/Services/Authorization/Middleware/index.d.ts +17 -0
  117. package/dist/src/Services/Authorization/activeGrants.d.ts +44 -0
  118. package/dist/src/Services/Authorization/activeRows.d.ts +47 -0
  119. package/dist/src/Services/Logger/ServerLogBuffer.d.ts +55 -3
  120. package/dist/src/Services/Logger/utils.d.ts +16 -0
  121. package/dist/src/Services/Notification/index.d.ts +1 -0
  122. package/dist/src/Services/Notification/source.d.ts +22 -0
  123. package/dist/src/Services/OAuth/OAuthService.d.ts +4 -0
  124. package/dist/src/Services/OAuth/index.d.ts +3 -1
  125. package/dist/src/Services/OAuth/profileSync.d.ts +34 -16
  126. package/dist/src/Services/OAuth/profileSyncHooks.d.ts +69 -0
  127. package/dist/src/Services/OAuth/profileSyncTargets.d.ts +49 -0
  128. package/dist/src/Services/OAuth/providers/microsoft.d.ts +4 -0
  129. package/dist/src/Services/OAuth/types.d.ts +41 -1
  130. package/dist/src/Services/Verification/cancelPhase.d.ts +76 -0
  131. package/dist/src/Services/Verification/decidingRequirement.d.ts +42 -0
  132. package/dist/src/Services/Verification/index.d.ts +22 -4
  133. package/dist/src/Services/Verification/reviewSteps.d.ts +21 -0
  134. package/dist/src/Services/Verification/startPhase.d.ts +26 -0
  135. package/dist/src/Services/Verification/supersession.d.ts +63 -0
  136. package/dist/src/Services/Verification/types.d.ts +73 -2
  137. package/dist/src/Utils/processRegistry.d.ts +23 -0
  138. package/dist/src/Utils/redactSecrets.d.ts +193 -0
  139. package/dist/src/Utils/redaction/bare.d.ts +65 -0
  140. package/dist/src/Utils/redaction/floor.d.ts +14 -0
  141. package/dist/src/Utils/redaction/freeText.d.ts +19 -0
  142. package/dist/src/Utils/redaction/json.d.ts +6 -0
  143. package/dist/src/Utils/redaction/lists.d.ts +29 -0
  144. package/dist/src/Utils/redaction/markers.d.ts +57 -0
  145. package/dist/src/Utils/redaction/names.d.ts +37 -0
  146. package/dist/src/Utils/redaction/pairs.d.ts +7 -0
  147. package/dist/src/Utils/redaction/secretString.d.ts +33 -0
  148. package/dist/src/Utils/redaction/structured.d.ts +37 -0
  149. package/dist/src/Utils/redaction/walk.d.ts +26 -0
  150. package/dist/src/types.d.ts +60 -8
  151. package/dist/test/jwt.d.ts +8 -0
  152. package/infra/templates/backend/package.json +22 -22
  153. package/infra/templates/backend/src/index.ts +33 -33
  154. package/package.json +1 -1
  155. package/public/components/docs/content/advanced.ts +506 -506
  156. package/public/components/docs/content/gettingStarted.ts +411 -411
  157. package/schemas/config.nucleus.json +25 -5
  158. package/schemas/nucleus.tables.schema.json +19 -19
  159. package/schemas/table.schema.json +509 -509
  160. package/scripts/build.ts +536 -533
  161. package/scripts/generate-schema.test.ts +399 -399
  162. package/scripts/generate-schema.ts +810 -810
  163. package/scripts/importCase.test.ts +174 -174
  164. package/scripts/scaffoldParity.test.ts +126 -126
  165. package/scripts/skillCitations.test.ts +2 -1
  166. package/scripts/version.ts +127 -127
  167. package/src/Services/Logger/auditTaxonomy.ts +154 -154
  168. package/src/Services/SchemaTables/resolveEntities.ts +214 -214
  169. package/src/system.tables.json +6035 -6035
package/README.md CHANGED
@@ -1,177 +1,177 @@
1
- # nucleus-core-ts
2
-
3
- A TypeScript framework for multi-tenant APIs on Bun: authentication and
4
- authorization, entity CRUD generated from a config file, storage, payments,
5
- chat, pub/sub, monitoring — and a typed client generated from the same
6
- declarations the server serves.
7
-
8
- ```bash
9
- bunx nucleus-core-ts init
10
- ```
11
-
12
- ## Where the documentation is
13
-
14
- This file is an index, not a manual. The maintained documentation is:
15
-
16
- - **`.claude/skills/`** — 21 skills, one per subsystem (`nucleus-authentication`,
17
- `nucleus-entities`, `nucleus-payment`, …). These are the reference.
18
- - **`public/`** — the docs site shipped in the package.
19
- - **`src/types.ts`** — the configuration surface, and the last word when a
20
- document and the code disagree.
21
-
22
- Other `.md` files in the repo are working notes or historical audit records.
23
- `docs/` in particular is an archive: it records what was true when it was
24
- written and is not updated.
25
-
26
- ## Entry points
27
-
28
- | import | what |
29
- |---|---|
30
- | `nucleus-core-ts` | the server plugin and the services it lends a host app |
31
- | `nucleus-core-ts/client` | the generated, typed API client |
32
- | `nucleus-core-ts/fe` | React components (auth pages, admin panels) |
33
- | `nucleus-core-ts/proxy` | server-side proxy client |
34
-
35
- `client`, `fe` and `proxy` are independent of the HTTP layer below.
36
-
37
- ## The HTTP layer is ours
38
-
39
- The server half used to run on [Elysia](https://elysiajs.com). It does not any
40
- more, and not partially: there is no adapter, no swagger plugin, no static
41
- plugin, no peer dependency, no import anywhere in the source, and nothing in
42
- `node_modules`. The reason was never performance — nucleus-core is a product of
43
- its own, and a product's HTTP layer, type system and public API shape should not
44
- belong to a third project we do not control.
45
-
46
- **What that means, concretely:**
47
-
48
- - **One entry point.** `createNucleusServer({ config, port })` serves the whole
49
- product on `Bun.serve`. `NucleusElysiaPlugin` is gone; a consumer that used it
50
- changes its boot line and its route modules and keeps everything else.
51
-
52
- - **A host brings its own routes.** This is what took longest and it is what
53
- every consumer was blocked on:
54
-
55
- ```ts
56
- await createNucleusServer({
57
- config: { options, schema, relations },
58
- port: 3000,
59
- routes: [{ prefix: '/reports', routes: [defineRoute({ … })] }],
60
- onRequest: (ctx) => Response | undefined, // BEFORE the inbound guard and auth
61
- socket: defineWebSocketRoute({ … }),
62
- onStop: [async () => { /* close your pool */ }],
63
- })
64
- ```
65
-
66
- A group's routes sit behind nucleus's auth, exactly as they did when mounted on
67
- an Elysia carrying the plugin — that is the inherited contract. Something that
68
- must answer without authentication goes in `onRequest`, which runs first: a
69
- public webhook that needs a verbatim body, an anonymous share link.
70
-
71
- - **"No Elysia" is measured, not asserted.** `noFrameworkGraph.test.ts` checks it
72
- two ways that miss different things: every entry point is bundled with packages
73
- left external and must contain no `from "elysia"`, and every source file plus
74
- the manifest is scanned, because a dynamic import never reaches a bundle.
75
-
76
- - **The suites that compared two servers now state one.** With no second arm to
77
- agree with, "the two answer alike" became "this is the answer" — and the claims
78
- got stronger doing it, from 53 tests and 189 assertions to 74 and 422. Parity
79
- passed whenever both arms were wrong in the same way; an absolute cannot.
80
-
81
- - `src/Server/` holds the replacement — response semantics, `defineRoute`, the
82
- router, the WebSocket upgrade — and every route in the repo declares against
83
- it. See the `nucleus-routing` skill for how to declare one.
84
-
85
- - The work, its measurements and its open questions live in
86
- [`ELYSIA_MIGRATION.md`](./ELYSIA_MIGRATION.md). Read that before touching the
87
- server's HTTP edge.
88
-
89
- **What the migration found.** Twenty defects that existed in the shipped product
90
- and that no test caught. Seventeen came from making the two paths answer the same
91
- question; the last three came from removing Elysia from the TESTS, which moved
92
- ~58 suites off Elysia's routing and validation and onto the router that actually
93
- serves them. The ones worth naming:
94
-
95
- | | |
96
- |---|---|
97
- | login sent ONE cookie | `access_token` and `refresh_token` never reached the browser |
98
- | the 3-D Secure callback was dead | `Body already used` since the bridge landed |
99
- | a resumable upload wrote nothing | `application/octet-stream` bodies arrived as `undefined`, so the chunk was appended as nothing and the request answered success |
100
- | auth ran AFTER validation | an unauthenticated caller was handed the schema of a protected endpoint — property names, types, and the sample the validator builds |
101
- | a thrown handler *or guard* leaked | Bun's `DevErrorPage` carried the raw error text where the product answers a JSON envelope whose whole job is withholding it |
102
- | 422s and 404s shipped bare | no `nosniff`, no HSTS, no `x-frame-options`, no request id — on 151 routes |
103
- | `stop()` leaked and killed | the DB pool, Redis and a live interval survived every restart, and in-flight requests were cut where Elysia drains them |
104
- | monitoring was blind | the request log, the collector and the live store recorded nothing on the Bun path |
105
- | form bodies were not parsed | `application/x-www-form-urlencoded` fell through every branch |
106
- | schema `default:` never applied | a defaulted field left out became a 422; so did an extra property, an absent optional body, and a malformed one |
107
- | nested form keys stayed flat | `a.b=1` arrived as a key called `a.b`, so `body.a.b` found nothing |
108
- | a WebSocket upgrade skipped the guards | no rate limit, no tenant resolution, no header strip |
109
-
110
- Two of those were the OTHER way round, with the SHIPPED path broken and the
111
- replacement correct: multipart uploads never worked on Elysia at all, and a
112
- failed insert came back from it as `500 text/plain` carrying the schema name,
113
- the table name and every column, because Elysia discarded the `Response` the
114
- product's error responder returned. Both are gone with the path that had them.
115
-
116
- **And three found by taking Elysia out of the tests** — all of them live on the
117
- only path that remains, all now fixed:
118
-
119
- | | |
120
- |---|---|
121
- | the 422 handed the caller their own submitted values | `POST /login` with a bad email answered `"password": "hunter2-the-real-one"` — into the response, and into any log that records one. Every value in a refusal is its TYPE now. The parity walk could not see it: it sends an empty body, so `found` was always `{}` |
122
- | a malformed body on a route with NO body schema | reached the handler as `undefined` and 500'd where the handler dereferenced it. 69 of 134 write declarations carry no body schema and 36 read the body anyway — a client error was becoming a server error. Now a 400 |
123
- | a JSON body claimed to be `text/plain` | seventeen returned-`Error` sites, including the whole authentication middleware. Copied from Elysia so the two arms agreed byte for byte; with Elysia gone the only argument for it went too |
124
-
125
- `ELYSIA_MIGRATION.md` §11 has the rest, including the four audit claims that did
126
- NOT reproduce when measured, and one regression this work introduced and then
127
- removed.
128
-
129
- Breaking changes will be announced here first. There is no 1.0 date; the 0.9.x
130
- line continues.
131
-
132
- ## Gates
133
-
134
- Every push runs, and all of these block:
135
-
136
- ```bash
137
- bun test src scripts fe # unit tests
138
- bunx tsc --noEmit -p tsconfig.check.json # the core library must type-check clean
139
- ```
140
-
141
- **The lint gate is currently not one of them.** CI runs `bunx biome check .`,
142
- and `biome` on npm is not Biome — it is an unrelated 0.3.3 package for managing
143
- environment variables. `@biomejs/biome` is the real one and is not declared as a
144
- dependency, so the step resolves the wrong package, prints no findings and exits
145
- 0. It passes on every commit and checks nothing. Locally it works only for
146
- whoever has Biome installed globally.
147
-
148
- Repairing it means declaring `@biomejs/biome`, pointing CI at it, and then
149
- making the repo clean under the real tool — which is a repo-wide change, so it
150
- is written down here rather than done quietly. See `ELYSIA_MIGRATION.md` §7.
151
-
152
- **What that costs, measured** (`bunx @biomejs/biome check .`, 1144 files):
153
-
154
- | | count |
155
- |---|---|
156
- | errors today | 234 |
157
- | fixed by `--write` | 90, touching 83 files |
158
- | left needing a judgement call | 144 errors + 29 warnings + 9 infos |
159
-
160
- The biggest groups are `assist/source/organizeImports` (33) and
161
- `lint/suspicious/noThenProperty` (12 — mostly chainable drizzle stubs in tests,
162
- where a `then` property is the point). So the repair is two commits, not one: a
163
- mechanical `--write` pass, then a smaller pass that decides what to keep.
164
-
165
- CI additionally runs every `*.integration.test.ts` against a real Postgres and
166
- Redis (`RUN_INTEGRATION=1`); they self-skip otherwise, because the races they
167
- catch — over-commit, non-atomic payout
168
- bookkeeping — cannot be reproduced against a fake backend.
169
-
170
- `tsconfig.check.json` covers `src` only — `fe/` imports from the package root,
171
- which only exists after a build, so it cannot be type-checked on a fresh
172
- checkout. The build emits its declarations best-effort.
173
-
174
- A local green is not a CI green. Path separators, case sensitivity, musl memory
175
- behaviour and `/proc` fixtures all differ between a developer machine and the
176
- Alpine image that runs in production; `ELYSIA_MIGRATION.md` §6 has the matrix
177
- and the container command that reproduces it.
1
+ # nucleus-core-ts
2
+
3
+ A TypeScript framework for multi-tenant APIs on Bun: authentication and
4
+ authorization, entity CRUD generated from a config file, storage, payments,
5
+ chat, pub/sub, monitoring — and a typed client generated from the same
6
+ declarations the server serves.
7
+
8
+ ```bash
9
+ bunx nucleus-core-ts init
10
+ ```
11
+
12
+ ## Where the documentation is
13
+
14
+ This file is an index, not a manual. The maintained documentation is:
15
+
16
+ - **`.claude/skills/`** — 21 skills, one per subsystem (`nucleus-authentication`,
17
+ `nucleus-entities`, `nucleus-payment`, …). These are the reference.
18
+ - **`public/`** — the docs site shipped in the package.
19
+ - **`src/types.ts`** — the configuration surface, and the last word when a
20
+ document and the code disagree.
21
+
22
+ Other `.md` files in the repo are working notes or historical audit records.
23
+ `docs/` in particular is an archive: it records what was true when it was
24
+ written and is not updated.
25
+
26
+ ## Entry points
27
+
28
+ | import | what |
29
+ |---|---|
30
+ | `nucleus-core-ts` | the server plugin and the services it lends a host app |
31
+ | `nucleus-core-ts/client` | the generated, typed API client |
32
+ | `nucleus-core-ts/fe` | React components (auth pages, admin panels) |
33
+ | `nucleus-core-ts/proxy` | server-side proxy client |
34
+
35
+ `client`, `fe` and `proxy` are independent of the HTTP layer below.
36
+
37
+ ## The HTTP layer is ours
38
+
39
+ The server half used to run on [Elysia](https://elysiajs.com). It does not any
40
+ more, and not partially: there is no adapter, no swagger plugin, no static
41
+ plugin, no peer dependency, no import anywhere in the source, and nothing in
42
+ `node_modules`. The reason was never performance — nucleus-core is a product of
43
+ its own, and a product's HTTP layer, type system and public API shape should not
44
+ belong to a third project we do not control.
45
+
46
+ **What that means, concretely:**
47
+
48
+ - **One entry point.** `createNucleusServer({ config, port })` serves the whole
49
+ product on `Bun.serve`. `NucleusElysiaPlugin` is gone; a consumer that used it
50
+ changes its boot line and its route modules and keeps everything else.
51
+
52
+ - **A host brings its own routes.** This is what took longest and it is what
53
+ every consumer was blocked on:
54
+
55
+ ```ts
56
+ await createNucleusServer({
57
+ config: { options, schema, relations },
58
+ port: 3000,
59
+ routes: [{ prefix: '/reports', routes: [defineRoute({ … })] }],
60
+ onRequest: (ctx) => Response | undefined, // BEFORE the inbound guard and auth
61
+ socket: defineWebSocketRoute({ … }),
62
+ onStop: [async () => { /* close your pool */ }],
63
+ })
64
+ ```
65
+
66
+ A group's routes sit behind nucleus's auth, exactly as they did when mounted on
67
+ an Elysia carrying the plugin — that is the inherited contract. Something that
68
+ must answer without authentication goes in `onRequest`, which runs first: a
69
+ public webhook that needs a verbatim body, an anonymous share link.
70
+
71
+ - **"No Elysia" is measured, not asserted.** `noFrameworkGraph.test.ts` checks it
72
+ two ways that miss different things: every entry point is bundled with packages
73
+ left external and must contain no `from "elysia"`, and every source file plus
74
+ the manifest is scanned, because a dynamic import never reaches a bundle.
75
+
76
+ - **The suites that compared two servers now state one.** With no second arm to
77
+ agree with, "the two answer alike" became "this is the answer" — and the claims
78
+ got stronger doing it, from 53 tests and 189 assertions to 74 and 422. Parity
79
+ passed whenever both arms were wrong in the same way; an absolute cannot.
80
+
81
+ - `src/Server/` holds the replacement — response semantics, `defineRoute`, the
82
+ router, the WebSocket upgrade — and every route in the repo declares against
83
+ it. See the `nucleus-routing` skill for how to declare one.
84
+
85
+ - The work, its measurements and its open questions live in
86
+ [`ELYSIA_MIGRATION.md`](./ELYSIA_MIGRATION.md). Read that before touching the
87
+ server's HTTP edge.
88
+
89
+ **What the migration found.** Twenty defects that existed in the shipped product
90
+ and that no test caught. Seventeen came from making the two paths answer the same
91
+ question; the last three came from removing Elysia from the TESTS, which moved
92
+ ~58 suites off Elysia's routing and validation and onto the router that actually
93
+ serves them. The ones worth naming:
94
+
95
+ | | |
96
+ |---|---|
97
+ | login sent ONE cookie | `access_token` and `refresh_token` never reached the browser |
98
+ | the 3-D Secure callback was dead | `Body already used` since the bridge landed |
99
+ | a resumable upload wrote nothing | `application/octet-stream` bodies arrived as `undefined`, so the chunk was appended as nothing and the request answered success |
100
+ | auth ran AFTER validation | an unauthenticated caller was handed the schema of a protected endpoint — property names, types, and the sample the validator builds |
101
+ | a thrown handler *or guard* leaked | Bun's `DevErrorPage` carried the raw error text where the product answers a JSON envelope whose whole job is withholding it |
102
+ | 422s and 404s shipped bare | no `nosniff`, no HSTS, no `x-frame-options`, no request id — on 151 routes |
103
+ | `stop()` leaked and killed | the DB pool, Redis and a live interval survived every restart, and in-flight requests were cut where Elysia drains them |
104
+ | monitoring was blind | the request log, the collector and the live store recorded nothing on the Bun path |
105
+ | form bodies were not parsed | `application/x-www-form-urlencoded` fell through every branch |
106
+ | schema `default:` never applied | a defaulted field left out became a 422; so did an extra property, an absent optional body, and a malformed one |
107
+ | nested form keys stayed flat | `a.b=1` arrived as a key called `a.b`, so `body.a.b` found nothing |
108
+ | a WebSocket upgrade skipped the guards | no rate limit, no tenant resolution, no header strip |
109
+
110
+ Two of those were the OTHER way round, with the SHIPPED path broken and the
111
+ replacement correct: multipart uploads never worked on Elysia at all, and a
112
+ failed insert came back from it as `500 text/plain` carrying the schema name,
113
+ the table name and every column, because Elysia discarded the `Response` the
114
+ product's error responder returned. Both are gone with the path that had them.
115
+
116
+ **And three found by taking Elysia out of the tests** — all of them live on the
117
+ only path that remains, all now fixed:
118
+
119
+ | | |
120
+ |---|---|
121
+ | the 422 handed the caller their own submitted values | `POST /login` with a bad email answered `"password": "hunter2-the-real-one"` — into the response, and into any log that records one. Every value in a refusal is its TYPE now. The parity walk could not see it: it sends an empty body, so `found` was always `{}` |
122
+ | a malformed body on a route with NO body schema | reached the handler as `undefined` and 500'd where the handler dereferenced it. 69 of 134 write declarations carry no body schema and 36 read the body anyway — a client error was becoming a server error. Now a 400 |
123
+ | a JSON body claimed to be `text/plain` | seventeen returned-`Error` sites, including the whole authentication middleware. Copied from Elysia so the two arms agreed byte for byte; with Elysia gone the only argument for it went too |
124
+
125
+ `ELYSIA_MIGRATION.md` §11 has the rest, including the four audit claims that did
126
+ NOT reproduce when measured, and one regression this work introduced and then
127
+ removed.
128
+
129
+ Breaking changes will be announced here first. There is no 1.0 date; the 0.9.x
130
+ line continues.
131
+
132
+ ## Gates
133
+
134
+ Every push runs, and all of these block:
135
+
136
+ ```bash
137
+ bun test src scripts fe # unit tests
138
+ bunx tsc --noEmit -p tsconfig.check.json # the core library must type-check clean
139
+ ```
140
+
141
+ **The lint gate is currently not one of them.** CI runs `bunx biome check .`,
142
+ and `biome` on npm is not Biome — it is an unrelated 0.3.3 package for managing
143
+ environment variables. `@biomejs/biome` is the real one and is not declared as a
144
+ dependency, so the step resolves the wrong package, prints no findings and exits
145
+ 0. It passes on every commit and checks nothing. Locally it works only for
146
+ whoever has Biome installed globally.
147
+
148
+ Repairing it means declaring `@biomejs/biome`, pointing CI at it, and then
149
+ making the repo clean under the real tool — which is a repo-wide change, so it
150
+ is written down here rather than done quietly. See `ELYSIA_MIGRATION.md` §7.
151
+
152
+ **What that costs, measured** (`bunx @biomejs/biome check .`, 1144 files):
153
+
154
+ | | count |
155
+ |---|---|
156
+ | errors today | 234 |
157
+ | fixed by `--write` | 90, touching 83 files |
158
+ | left needing a judgement call | 144 errors + 29 warnings + 9 infos |
159
+
160
+ The biggest groups are `assist/source/organizeImports` (33) and
161
+ `lint/suspicious/noThenProperty` (12 — mostly chainable drizzle stubs in tests,
162
+ where a `then` property is the point). So the repair is two commits, not one: a
163
+ mechanical `--write` pass, then a smaller pass that decides what to keep.
164
+
165
+ CI additionally runs every `*.integration.test.ts` against a real Postgres and
166
+ Redis (`RUN_INTEGRATION=1`); they self-skip otherwise, because the races they
167
+ catch — over-commit, non-atomic payout
168
+ bookkeeping — cannot be reproduced against a fake backend.
169
+
170
+ `tsconfig.check.json` covers `src` only — `fe/` imports from the package root,
171
+ which only exists after a build, so it cannot be type-checked on a fresh
172
+ checkout. The build emits its declarations best-effort.
173
+
174
+ A local green is not a CI green. Path separators, case sensitivity, musl memory
175
+ behaviour and `/proc` fixtures all differ between a developer machine and the
176
+ Alpine image that runs in production; `ELYSIA_MIGRATION.md` §6 has the matrix
177
+ and the container command that reproduces it.
package/bin/cli.ts CHANGED
@@ -1,136 +1,136 @@
1
- #!/usr/bin/env bun
2
-
3
- /**
4
- * Nucleus CLI — the single front door to nucleus-core-ts.
5
- *
6
- * nucleus init scaffold a new project (interactive)
7
- * nucleus dev … run locally (up | down | run)
8
- * nucleus add … extend an existing project (entity | feature | oauth)
9
- * nucleus validate validate config.json against the schema
10
- * nucleus doctor check the local environment (bun, postgres, redis, …)
11
- * nucleus generate generate the Drizzle schema from config.json
12
- */
13
-
14
- import { spawn } from 'node:child_process'
15
- import { dirname, join } from 'node:path'
16
- import { fileURLToPath } from 'node:url'
17
- import { addCommand } from './lib/commands/add'
18
- import { devCommand } from './lib/commands/dev'
19
- import { doctorCommand } from './lib/commands/doctor'
20
- import { initCommand } from './lib/commands/init'
21
- import { validateCommand } from './lib/commands/validate'
22
- import { banner, c, log } from './lib/ui'
23
-
24
- const here = dirname(fileURLToPath(import.meta.url))
25
- const rootDir = join(here, '..')
26
-
27
- const argv = process.argv.slice(2)
28
- const firstArg = argv[0] ?? ''
29
- const looksLikeFile =
30
- firstArg.endsWith('.json') || firstArg.startsWith('./') || firstArg.startsWith('/')
31
- const command = looksLikeFile ? 'generate' : firstArg
32
-
33
- async function pkgVersion(): Promise<string> {
34
- try {
35
- return (JSON.parse(await Bun.file(join(rootDir, 'package.json')).text()) as { version: string })
36
- .version
37
- } catch {
38
- return '?'
39
- }
40
- }
41
-
42
- function showHelp(version: string) {
43
- banner(`Nucleus CLI v${version}`, 'nucleus-core-ts — config-driven backend framework')
44
- log(`
45
- ${c.bold('Usage')} ${c.cyan('nucleus')} ${c.dim('<command> [options]')}
46
-
47
- ${c.bold('Create & evolve')}
48
- ${c.cyan('init')} ${c.dim('[--preset <p>] [-o <dir>] [-y]')} build a config + scaffold a runnable backend (zero-to-running)
49
- ${c.cyan('add')} ${c.dim('entity | feature <n> | oauth <p>')} extend this project's config in place (+ regenerate)
50
- ${c.cyan('scaffold')} full-project scaffold (backend+frontend+k8s+pipelines) from a config.json
51
- ${c.cyan('generate')} ${c.dim('<config.json> <outDir>')} generate the Drizzle schema + relations from config
52
- ${c.cyan('validate')} ${c.dim('[config.json]')} validate a config against the JSON-Schema + cross-field rules
53
-
54
- ${c.bold('Local dev')}
55
- ${c.cyan('dev')} ${c.dim('[up | down]')} run the app; ${c.dim('up')} starts Postgres+Redis, ${c.dim('down')} stops them
56
- ${c.cyan('doctor')} check the local environment can run a Nucleus project
57
-
58
- ${c.bold('Maintenance')}
59
- ${c.cyan('audit:purge-noise')} ${c.dim('[--execute]')} delete historical low-signal audit_logs rows (dry-run by default)
60
- ${c.cyan('help')}${c.dim(', ')}${c.cyan('--version')} this help / the installed version
61
-
62
- ${c.dim('init = preset → runnable backend · scaffold (= new) = full project · Aliases: generate = gen, validate = check')}
63
- `)
64
- }
65
-
66
- function runScript(scriptRelPath: string, args: string[]): Promise<never> {
67
- return new Promise<never>(() => {
68
- const proc = spawn('bun', ['run', join(rootDir, scriptRelPath), ...args], {
69
- cwd: process.cwd(),
70
- stdio: 'inherit',
71
- })
72
- proc.on('close', (code) => process.exit(code ?? 1))
73
- })
74
- }
75
-
76
- switch (command) {
77
- case 'init':
78
- process.exit(await initCommand(argv.slice(1)))
79
- break
80
-
81
- case 'dev':
82
- process.exit(await devCommand(argv.slice(1)))
83
- break
84
-
85
- case 'add':
86
- process.exit(await addCommand(argv.slice(1)))
87
- break
88
-
89
- case 'scaffold':
90
- case 'new': {
91
- // Full-project scaffold (backend + frontend + k8s + pipelines) from an
92
- // existing config.json. `init` is the leaner, config-building local-dev path.
93
- const { scaffold } = await import(join(rootDir, 'infra', 'scripts', 'generate-project.ts'))
94
- await scaffold(join(rootDir, 'infra'))
95
- break
96
- }
97
-
98
- case 'generate':
99
- case 'gen': {
100
- const genArgs = looksLikeFile ? argv : argv.slice(1)
101
- await runScript(join('scripts', 'generate-schema.ts'), genArgs)
102
- break
103
- }
104
-
105
- case 'validate':
106
- case 'check':
107
- process.exit(await validateCommand(argv.slice(1)))
108
- break
109
-
110
- case 'doctor':
111
- process.exit(await doctorCommand())
112
- break
113
-
114
- case 'audit:purge-noise':
115
- case 'audit-purge-noise':
116
- await runScript(join('scripts', 'audit-purge-noise.ts'), argv.slice(1))
117
- break
118
-
119
- case '--version':
120
- case '-v':
121
- log(await pkgVersion())
122
- break
123
-
124
- case 'help':
125
- case '--help':
126
- case '-h':
127
- case '':
128
- case undefined:
129
- showHelp(await pkgVersion())
130
- break
131
-
132
- default:
133
- log(`${c.red('Unknown command:')} ${command}\n`)
134
- showHelp(await pkgVersion())
135
- process.exit(1)
136
- }
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Nucleus CLI — the single front door to nucleus-core-ts.
5
+ *
6
+ * nucleus init scaffold a new project (interactive)
7
+ * nucleus dev … run locally (up | down | run)
8
+ * nucleus add … extend an existing project (entity | feature | oauth)
9
+ * nucleus validate validate config.json against the schema
10
+ * nucleus doctor check the local environment (bun, postgres, redis, …)
11
+ * nucleus generate generate the Drizzle schema from config.json
12
+ */
13
+
14
+ import { spawn } from 'node:child_process'
15
+ import { dirname, join } from 'node:path'
16
+ import { fileURLToPath } from 'node:url'
17
+ import { addCommand } from './lib/commands/add'
18
+ import { devCommand } from './lib/commands/dev'
19
+ import { doctorCommand } from './lib/commands/doctor'
20
+ import { initCommand } from './lib/commands/init'
21
+ import { validateCommand } from './lib/commands/validate'
22
+ import { banner, c, log } from './lib/ui'
23
+
24
+ const here = dirname(fileURLToPath(import.meta.url))
25
+ const rootDir = join(here, '..')
26
+
27
+ const argv = process.argv.slice(2)
28
+ const firstArg = argv[0] ?? ''
29
+ const looksLikeFile =
30
+ firstArg.endsWith('.json') || firstArg.startsWith('./') || firstArg.startsWith('/')
31
+ const command = looksLikeFile ? 'generate' : firstArg
32
+
33
+ async function pkgVersion(): Promise<string> {
34
+ try {
35
+ return (JSON.parse(await Bun.file(join(rootDir, 'package.json')).text()) as { version: string })
36
+ .version
37
+ } catch {
38
+ return '?'
39
+ }
40
+ }
41
+
42
+ function showHelp(version: string) {
43
+ banner(`Nucleus CLI v${version}`, 'nucleus-core-ts — config-driven backend framework')
44
+ log(`
45
+ ${c.bold('Usage')} ${c.cyan('nucleus')} ${c.dim('<command> [options]')}
46
+
47
+ ${c.bold('Create & evolve')}
48
+ ${c.cyan('init')} ${c.dim('[--preset <p>] [-o <dir>] [-y]')} build a config + scaffold a runnable backend (zero-to-running)
49
+ ${c.cyan('add')} ${c.dim('entity | feature <n> | oauth <p>')} extend this project's config in place (+ regenerate)
50
+ ${c.cyan('scaffold')} full-project scaffold (backend+frontend+k8s+pipelines) from a config.json
51
+ ${c.cyan('generate')} ${c.dim('<config.json> <outDir>')} generate the Drizzle schema + relations from config
52
+ ${c.cyan('validate')} ${c.dim('[config.json]')} validate a config against the JSON-Schema + cross-field rules
53
+
54
+ ${c.bold('Local dev')}
55
+ ${c.cyan('dev')} ${c.dim('[up | down]')} run the app; ${c.dim('up')} starts Postgres+Redis, ${c.dim('down')} stops them
56
+ ${c.cyan('doctor')} check the local environment can run a Nucleus project
57
+
58
+ ${c.bold('Maintenance')}
59
+ ${c.cyan('audit:purge-noise')} ${c.dim('[--execute]')} delete historical low-signal audit_logs rows (dry-run by default)
60
+ ${c.cyan('help')}${c.dim(', ')}${c.cyan('--version')} this help / the installed version
61
+
62
+ ${c.dim('init = preset → runnable backend · scaffold (= new) = full project · Aliases: generate = gen, validate = check')}
63
+ `)
64
+ }
65
+
66
+ function runScript(scriptRelPath: string, args: string[]): Promise<never> {
67
+ return new Promise<never>(() => {
68
+ const proc = spawn('bun', ['run', join(rootDir, scriptRelPath), ...args], {
69
+ cwd: process.cwd(),
70
+ stdio: 'inherit',
71
+ })
72
+ proc.on('close', (code) => process.exit(code ?? 1))
73
+ })
74
+ }
75
+
76
+ switch (command) {
77
+ case 'init':
78
+ process.exit(await initCommand(argv.slice(1)))
79
+ break
80
+
81
+ case 'dev':
82
+ process.exit(await devCommand(argv.slice(1)))
83
+ break
84
+
85
+ case 'add':
86
+ process.exit(await addCommand(argv.slice(1)))
87
+ break
88
+
89
+ case 'scaffold':
90
+ case 'new': {
91
+ // Full-project scaffold (backend + frontend + k8s + pipelines) from an
92
+ // existing config.json. `init` is the leaner, config-building local-dev path.
93
+ const { scaffold } = await import(join(rootDir, 'infra', 'scripts', 'generate-project.ts'))
94
+ await scaffold(join(rootDir, 'infra'))
95
+ break
96
+ }
97
+
98
+ case 'generate':
99
+ case 'gen': {
100
+ const genArgs = looksLikeFile ? argv : argv.slice(1)
101
+ await runScript(join('scripts', 'generate-schema.ts'), genArgs)
102
+ break
103
+ }
104
+
105
+ case 'validate':
106
+ case 'check':
107
+ process.exit(await validateCommand(argv.slice(1)))
108
+ break
109
+
110
+ case 'doctor':
111
+ process.exit(await doctorCommand())
112
+ break
113
+
114
+ case 'audit:purge-noise':
115
+ case 'audit-purge-noise':
116
+ await runScript(join('scripts', 'audit-purge-noise.ts'), argv.slice(1))
117
+ break
118
+
119
+ case '--version':
120
+ case '-v':
121
+ log(await pkgVersion())
122
+ break
123
+
124
+ case 'help':
125
+ case '--help':
126
+ case '-h':
127
+ case '':
128
+ case undefined:
129
+ showHelp(await pkgVersion())
130
+ break
131
+
132
+ default:
133
+ log(`${c.red('Unknown command:')} ${command}\n`)
134
+ showHelp(await pkgVersion())
135
+ process.exit(1)
136
+ }