@jskit-ai/agent-docs 0.1.270 → 0.1.272

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 (48) hide show
  1. package/guide/agent/app-extras/assistant-conversation.md +3 -2
  2. package/guide/agent/app-extras/assistant.md +35 -0
  3. package/guide/agent/app-setup/authentication.md +149 -0
  4. package/guide/agent/app-setup/database-layer.md +4 -0
  5. package/guide/agent/app-setup/initial-scaffolding.md +76 -0
  6. package/guide/agent/framework/typed-features.md +5 -0
  7. package/package.json +1 -1
  8. package/patterns/feature-package/example/booking-engine/package.json +1 -1
  9. package/patterns/minimal-foundation/PATTERN.md +3 -1
  10. package/patterns/minimal-foundation/example/package.json +4 -4
  11. package/patterns/minimal-foundation/example/server.js +9 -6
  12. package/patterns/minimal-foundation/example/tests/server/smoke.test.js +3 -4
  13. package/patterns/shell-foundation/example/package.json +5 -5
  14. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  15. package/patterns/shell-foundation/example/server.js +9 -6
  16. package/patterns/shell-foundation/example/tests/server/smoke.test.js +3 -4
  17. package/reference/autogen/PATTERN_INDEX.md +37 -37
  18. package/reference/autogen/packages/agent.md +1 -1
  19. package/reference/autogen/packages/assistant-runtime.md +1 -0
  20. package/reference/autogen/packages/auth-core.md +9 -2
  21. package/reference/autogen/packages/auth-provider-local-core.md +85 -21
  22. package/reference/autogen/packages/auth-provider-local-db-core.md +7 -0
  23. package/reference/autogen/packages/auth-web.md +13 -2
  24. package/reference/autogen/packages/crud-core.md +3 -2
  25. package/reference/autogen/packages/kernel.md +39 -1
  26. package/skills/jskit/references/pattern-index.md +37 -37
  27. package/skills/jskit/references/patterns/app/minimal-foundation/PATTERN.md +3 -1
  28. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
  29. package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +9 -6
  30. package/skills/jskit/references/patterns/app/minimal-foundation/example/tests/server/smoke.test.js +3 -4
  31. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
  32. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  33. package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +9 -6
  34. package/skills/jskit/references/patterns/app/shell-foundation/example/tests/server/smoke.test.js +3 -4
  35. package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -0
  36. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
  37. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  38. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  39. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  40. package/skills/jskit/references/patterns/crud/json-api-resource-package/PATTERN.md +11 -0
  41. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +2 -2
  42. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +1 -1
  43. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +1 -1
  44. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  45. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  46. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +2 -2
  47. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +3 -3
  48. package/skills/jskit/references/typed-features.md +5 -0
@@ -2473,8 +2473,9 @@ accounts) authorizes the same connection again and stops the reply when access i
2473
2473
  gone. A configured `effort` reaches every provider as the AI SDK's standard
2474
2474
  reasoning setting; without one, the engine's default applies (adaptive thinking at
2475
2475
  high effort for Anthropic-format models). The system prompt is the configured
2476
- instructions, then the summary, then the engine's state note with the current UTC
2477
- time.
2476
+ instructions, then the summary. The engine's state note with the current UTC time
2477
+ follows the conversation as its last message, so the provider's prompt cache keeps
2478
+ the whole history from one turn to the next.
2478
2479
 
2479
2480
  An application can set server-owned `limits.maxApiToolProgressCharacters` to a
2480
2481
  positive safe integer to bound the decoded text accompanying confirmed API tool
@@ -164,6 +164,41 @@ Workspace scope is valid only when both the runtime and its settings surface
164
164
  are workspace-aware. Requests must retain the selected workspace through the
165
165
  server action boundary.
166
166
 
167
+ ### Who may change the instructions
168
+
169
+ The settings hold the instructions every conversation on the surface follows,
170
+ so changing them needs more than reading them. Any signed-in user may read
171
+ them (a workspace settings surface also needs `workspace.settings.view`, a
172
+ console-owner surface the console owner). Changing them needs:
173
+
174
+ | Settings surface | Permission that changes the instructions |
175
+ | --- | --- |
176
+ | requires a workspace | `workspace.settings.update` |
177
+ | console-owner access | the console owner |
178
+ | any other | `assistant.settings.update` |
179
+
180
+ Grant `assistant.settings.update` to the application's administrators through
181
+ the request context's permissions, or name another permission the application
182
+ already grants in server config:
183
+
184
+ ```js
185
+ // config/server.js
186
+ export const config = {
187
+ assistantServer: {
188
+ home: {
189
+ aiIntegrationId: "assistant",
190
+ settingsUpdatePermission: "app.admin"
191
+ // or { require: "any", permissions: ["app.admin", "app.owner"] }
192
+ }
193
+ }
194
+ };
195
+ ```
196
+
197
+ `settingsUpdatePermission: { require: "authenticated" }` lets every signed-in
198
+ user change the instructions; choose it only deliberately. A malformed value
199
+ stops the server from starting. Anyone else is refused with
200
+ `assistant.runtime.settings.updateForbidden` (403).
201
+
167
202
  ### Existing SQL conversations
168
203
 
169
204
  The optional database integration uses the common conversation runtime for
@@ -66,6 +66,150 @@ Protected server operations declare an auth policy; they do not trust a hidden
66
66
  button or a page-local session check. Other packages extend auth through the
67
67
  public extension capability rather than replacing the auth service.
68
68
 
69
+ ## Sign-in protection
70
+
71
+ Two limits slow down password guessing, and both are on by default.
72
+
73
+ **Per client address.** Every auth route that can be abused by repetition
74
+ declares a `rateLimit`, which the kernel enforces for each client address. A
75
+ request over it is refused with status 429, the code `kernel.http.rateLimited`
76
+ (`params.count` is the seconds to wait) and a `Retry-After` header. The
77
+ defaults, per minute: `login`, `register`, `registerConfirm`,
78
+ `loginOtpRequest` and `loginOtpVerify` 10; `registerConfirmationResend` and
79
+ `passwordForgot` 5;
80
+ `oauthStart`, `oauthComplete`, `passwordRecovery` and `passwordReset` 20;
81
+ `devLoginAs` 30. Change one in the application's server configuration;
82
+ `false` removes it:
83
+
84
+ ```js
85
+ // config/server.js
86
+ config.auth = {
87
+ ...config.auth,
88
+ rateLimits: {
89
+ login: { max: 5, timeWindow: "1 minute" },
90
+ oauthStart: false
91
+ }
92
+ };
93
+ ```
94
+
95
+ A route name auth does not have, or a value that is neither
96
+ `{ max, timeWindow }` nor `false`, stops startup. `timeWindow` is milliseconds
97
+ or text such as `"30 seconds"` or `"1 hour"`.
98
+
99
+ The address is Fastify's `request.ip`. Behind a reverse proxy every request
100
+ comes from the proxy, so all clients would share one count: create Fastify with
101
+ `trustProxy` set to the proxy's address (for example
102
+ `Fastify({ trustProxy: "127.0.0.1" })`), and never to `true` when clients can
103
+ also reach the server directly.
104
+
105
+ **Per account (local auth).** After 5 failed sign-ins in a row for one email,
106
+ that email must wait 30 seconds before its next attempt; each further failure
107
+ doubles the wait, up to an hour (NIST SP 800-63B's example). While it waits,
108
+ sign-in is refused, even with the right password, with status 429, the code
109
+ `auth.local.login.locked` (`params.count` minutes, `params.retryAfterSeconds`)
110
+ and `Retry-After`. Emails without an account are counted the same way, so the
111
+ wait says nothing about which accounts exist. A successful sign-in forgets the
112
+ failures, and so does a day without one. Waits are temporary and password
113
+ recovery is never blocked: completing a password reset ends the wait, so an
114
+ attacker who keeps an account waiting cannot keep its owner out. The failures
115
+ are kept in the file store (`sign-in-failures.passwd`) or, with
116
+ `@jskit-ai/auth-provider-local-db-core`, in the `auth_local_sign_in_failures`
117
+ table (run `npm run db:migrate`).
118
+
119
+ | Env | Default | Meaning |
120
+ | --- | --- | --- |
121
+ | `AUTH_LOCAL_SIGN_IN_FAILURES_BEFORE_WAIT` | `5` | Failed sign-ins in a row before the account waits, 1 to 100. |
122
+ | `AUTH_LOCAL_SIGN_IN_FIRST_WAIT_SECONDS` | `30` | The first wait. |
123
+ | `AUTH_LOCAL_SIGN_IN_MAX_WAIT_SECONDS` | `3600` | The longest wait, at least the first. |
124
+ | `AUTH_LOCAL_PASSWORD_MIN_LENGTH` | `8` | The shortest password local auth accepts, 8 to 64. |
125
+
126
+ NIST SP 800-63B-4 asks for at least 15 characters when a password is the only
127
+ sign-in factor, and at least 8 when it is used with another factor; set
128
+ `AUTH_LOCAL_PASSWORD_MIN_LENGTH=15` for password-only sign-in. Passwords up to
129
+ 128 characters are accepted.
130
+
131
+ The login page shows either refusal, translated, with the wait.
132
+
133
+ ## Email confirmation (local auth)
134
+
135
+ Local auth confirms the email address of each account it registers by
136
+ password, in every environment, unless `AUTH_LOCAL_EMAIL_CONFIRMATION=false`.
137
+ Registering keeps the registration waiting, with no account, profile or
138
+ session, and answers `requiresEmailConfirmation: true`; the sign-in page then
139
+ shows "Confirm your email", with a button to send a new link and a field for
140
+ the link or its code. Opening the link (`/auth/login?confirmation_token=...`)
141
+ creates the account and signs the person in. The `auth.local.user.registered`
142
+ hook runs then, not on registering. Signing in with the right password while the registration waits
143
+ is refused with status 403 and `auth.login.emailUnconfirmed`, and the sign-in
144
+ page shows how to confirm; a wrong password is an ordinary failed sign-in.
145
+
146
+ A link's code is random, kept only as a hash, works once and expires after a
147
+ day. Registering the same email again, or asking for a new link, replaces it,
148
+ so a waiting registration never keeps the email from its owner. Asking for a
149
+ new link answers the same whether or not a registration waits.
150
+
151
+ The link is emailed by the optional `auth.local.confirmation-sender`
152
+ capability, an async function accepting `{ email, confirmationUrl }`, built
153
+ like the recovery sender below; with it, `APP_PUBLIC_URL` is required and the
154
+ link is never returned or logged. In development without one, the link is in
155
+ the registration's answer (`confirmationUrl`) and the log, and the sign-in page
156
+ shows it with a button that confirms it. In production (`NODE_ENV=production`)
157
+ without one, the server refuses to start, naming both ways out: install a
158
+ sender, or set `AUTH_LOCAL_EMAIL_CONFIRMATION=false`. For direct library
159
+ composition, pass the sender as `confirmationSender` to
160
+ `createLocalAuthService()`, with `emailConfirmationSender: true` in
161
+ `resolveLocalAuthConfig()`; without one, pass
162
+ `createLoggedConfirmationSender(logger)`.
163
+
164
+ | Env | Default | Meaning |
165
+ | --- | --- | --- |
166
+ | `AUTH_LOCAL_EMAIL_CONFIRMATION` | on | `false` registers and signs in at once, without confirming the email. |
167
+
168
+ Accounts that exist already are confirmed: only registrations made after the
169
+ upgrade wait. Waiting registrations are kept in the file store
170
+ (`registrations.passwd`) or, with `@jskit-ai/auth-provider-local-db-core`, in
171
+ the `auth_local_registrations` table (run `npm run db:migrate`). A custom
172
+ `auth.local.backend` implements the `registrations` collection. Tests that
173
+ register and expect a session either confirm with the answer's
174
+ `confirmationUrl` or set `AUTH_LOCAL_EMAIL_CONFIRMATION=false`.
175
+
176
+ ## The first account in production (local auth)
177
+
178
+ The first account to register becomes the application's owner, so on a
179
+ production server (`NODE_ENV=production`) whose local auth has no account yet,
180
+ registering needs a one-time setup code that only the server's operator can
181
+ read. Development needs no code.
182
+
183
+ Each start of such a server makes a new code, such as
184
+ `7KQ2M-X9D4F-0RWHN-3TBZC`, and keeps only its hash. It writes the code:
185
+
186
+ - in the log, as one warning: `Local auth has no account yet: register the
187
+ first account with this setup code.`, with the fields
188
+ `firstAccountSetupCode` (the code) and `firstAccountSetupCodeFile`;
189
+ - in the file `first-account-setup-code` in `AUTH_LOCAL_STORE_DIR` (default
190
+ `.jskit/auth`), mode 600, for a hosting tool to read. A store directory that
191
+ cannot be written leaves only the log line.
192
+
193
+ While it waits, the sign-in page's registration form shows a "Setup code"
194
+ field (capability `features.firstAccountSetupCode`), and the register body
195
+ takes `setupCode`; letter case, spaces and dashes do not matter. A missing code
196
+ is refused with 403 `auth.local.setup.codeRequired`, a wrong one with 403
197
+ `auth.local.setup.codeInvalid`. An OAuth sign-in cannot create that first
198
+ account. The registration then follows the ordinary rules, email confirmation
199
+ included, and confirming it signs the owner in. The code admits registrations
200
+ until an account exists; then it stops working and its file is deleted, and
201
+ registration needs no code again. Restarting before then makes a new code and
202
+ the old one stops working: the latest log line and the file always agree, and
203
+ nothing but a hash is kept for checking it. Run one server while the first
204
+ account is set up, since each server makes its own code.
205
+
206
+ A hosting tool that shows the code to the operator itself supplies it in
207
+ `AUTH_LOCAL_SETUP_CODE`, at least 20 letters and digits, for example a
208
+ generated production secret. The server then asks for that code at every start
209
+ and on every server, writes no `first-account-setup-code` file, and logs only
210
+ that the code is in `AUTH_LOCAL_SETUP_CODE`, never the code itself. Outside
211
+ production the variable is ignored.
212
+
69
213
  ## Application-owned recovery email
70
214
 
71
215
  Local auth owns recovery tokens, expiry and recovery-scoped sessions. The
@@ -128,6 +272,11 @@ URL. Without a sender, production recovery is disabled; explicit development
128
272
  output remains available for local testing. A sender failure never falls back
129
273
  to development output. The generic recovery delivery capability is `email`.
130
274
 
275
+ Production is `NODE_ENV=production`; anything else, unset included, is
276
+ development, so the host that serves the application sets it. A production
277
+ server with `AUTH_LOCAL_RECOVERY_DEV_OUTPUT` set refuses to start and names the
278
+ setting to remove, rather than ignoring it.
279
+
131
280
  ### Pre-release migration
132
281
 
133
282
  This is a V0 breaking change. There is one runtime path, with no old SMTP reader
@@ -45,6 +45,10 @@ also derives its dialect from the installed driver and rejects a conflicting
45
45
  The application or hosting environment creates the database and provides its
46
46
  credentials. JSKIT never guesses or commits them.
47
47
 
48
+ The running database provider registers a `database` health check that runs
49
+ `select 1` within two seconds. The foundations' `GET /api/health` answers `503`
50
+ while it fails; see [Health endpoint](./initial-scaffolding.md#health-endpoint).
51
+
48
52
  ## Migration configuration
49
53
 
50
54
  Use the fixed-dialect pattern owned by the selected driver. The important
@@ -100,6 +100,82 @@ loading. Do not replace them with generic spinners. Keep failures local when a
100
100
  resource cannot render; use shared toast/snackbar feedback for transient user
101
101
  commands so errors do not push the working page down.
102
102
 
103
+ ## Health endpoint
104
+
105
+ Both foundations register `GET /api/health` after `createInstalledRuntime` and
106
+ answer from the installed runtime's `health.check()`: `200` with
107
+ `{ ok: true, checks, app }` when every check the installed packages register
108
+ passes, and `503` with `ok: false` when one fails. `@jskit-ai/database-runtime`
109
+ registers a `database` check that runs `select 1`. Each check has two seconds;
110
+ a failure is logged on the server and the response only names which checks
111
+ passed, so the route can stay public for load balancers and uptime monitors.
112
+ Requests that arrive while a check runs share it. An application that adopted
113
+ an older foundation, where the route answered `ok: true` before the runtime was
114
+ created, moves the route after `createInstalledRuntime` in the same way.
115
+
116
+ A package adds its own check through the optional `runtime.health`
117
+ capability: `optional: { health: "runtime.health" }`, then
118
+ `health?.register("queue", () => queue.ping())` in `setup()`. The check resolves
119
+ when healthy and rejects or hangs when not.
120
+
121
+ ## Security headers
122
+
123
+ `createInstalledRuntime` adds security headers to every response its Fastify
124
+ server sends, including error and not-found responses. A route that sets one of
125
+ these headers itself keeps its own value.
126
+
127
+ | Header | When | Default |
128
+ | --- | --- | --- |
129
+ | `X-Content-Type-Options: nosniff` | always | on |
130
+ | `Referrer-Policy` | always | `strict-origin-when-cross-origin` |
131
+ | `Strict-Transport-Security` | production | `max-age=31536000` |
132
+ | `Content-Security-Policy: frame-ancestors` | production | `'self'` |
133
+ | `Content-Security-Policy-Report-Only` | production | the content policy below |
134
+
135
+ Production means `NODE_ENV=production`. A host that starts the server with a
136
+ plain `npm start` runs it in development; its Serve command must set the
137
+ variable, for example `env NODE_ENV=production npm start`. Development sends only
138
+ nosniff and the Referrer-Policy, so an editor's preview, such as Vibe64's, can
139
+ frame the application and inject its bridge script.
140
+
141
+ The content policy allows JSKIT's client: the application's own scripts and
142
+ WebAssembly, inline styles (Vuetify and the loading shell use them), images
143
+ from the application, `data:`, `blob:` and any HTTPS source, fonts and media
144
+ from the application and `data:`, same-origin requests and sockets, and no
145
+ plugins, foreign base URLs or foreign form posts. It is reported, not enforced,
146
+ by default: browsers log violations in the console without blocking anything,
147
+ because an application's own additions, such as a sign-in provider, an
148
+ analytics script or a map, are not known to JSKIT. Enforce it once the
149
+ application's pages run without violations. Framing is always enforced, since
150
+ browsers ignore `frame-ancestors` in a report-only policy.
151
+
152
+ Change the defaults in `config/server.js`:
153
+
154
+ ```js
155
+ export const config = {
156
+ securityHeaders: {
157
+ // Another site that frames production, such as the editor that previews it.
158
+ frameAncestors: ["'self'", "https://editor.example.com"],
159
+ contentSecurityPolicy: {
160
+ mode: "enforce", // "report-only" (default), "enforce" or "off"
161
+ directives: {
162
+ "connect-src": ["'self'", "https://auth.example.com"],
163
+ "form-action": false // false leaves a directive out
164
+ }
165
+ }
166
+ }
167
+ };
168
+ ```
169
+
170
+ `noSniff: false`, `referrerPolicy: false`, `strictTransportSecurity: false` and
171
+ `frameAncestors: false` leave that header out; `referrerPolicy` and
172
+ `strictTransportSecurity` also take their own header value. A directive's
173
+ sources replace JSKIT's for that directive; framing is set only through
174
+ `frameAncestors`. An invalid value stops the server from starting. A server
175
+ composed without `createInstalledRuntime` calls
176
+ `registerSecurityHeaders(fastify, { config, env })` from
177
+ `@jskit-ai/kernel/server/http` before it registers any plugin.
178
+
103
179
  ## Verification
104
180
 
105
181
  Pattern contract tests keep both examples concrete, parseable, npm-published,
@@ -735,6 +735,11 @@ for anything that reads or writes the domain.**
735
735
  `request.executeAction({ actionId: ACTION_X, input })` returns.
736
736
  - A route that bypasses actions (a provider's callback) gets its own named
737
737
  registrar. Summaries are sentences.
738
+ - A route that repetition can abuse (sign-in, codes, sending email) declares
739
+ `rateLimit: { max, timeWindow }`, which the kernel enforces per client
740
+ address and refuses with 429, `kernel.http.rateLimited` and `Retry-After`.
741
+ A limit worth changing per application is read from its config, as
742
+ `config.auth.rateLimits` is.
738
743
 
739
744
  Why: an action is the one place a domain operation's policy, input and
740
745
  events are declared.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.270",
3
+ "version": "0.1.272",
4
4
  "description": "Distributed JSKIT agent references, prompts, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -5,7 +5,7 @@
5
5
  "private": true,
6
6
  "type": "module",
7
7
  "dependencies": {
8
- "@jskit-ai/kernel": "0.2.7",
8
+ "@jskit-ai/kernel": "0.2.9",
9
9
  "json-rest-schema": "^1.0.17"
10
10
  },
11
11
  "exports": {
@@ -146,7 +146,9 @@ verification, not only against Vite development mode.
146
146
  After adapting the example, install the declared packages once and run
147
147
  `npm run develop` for the live application. Then run the application's lint,
148
148
  server tests, client tests, production build, and focused browser smoke.
149
- Confirm `/api/health` and the first product route.
149
+ Confirm `/api/health` and the first product route. The health route answers
150
+ from the installed runtime's `health.check()` after `createInstalledRuntime`, so
151
+ it returns `503` while a registered check, such as the database's, fails.
150
152
 
151
153
  ## Avoid
152
154
 
@@ -34,22 +34,22 @@
34
34
  "dependencies": {
35
35
  "@local/main": "0.1.0",
36
36
  "@fastify/static": "^10.1.3",
37
- "@jskit-ai/kernel": "0.2.7",
37
+ "@jskit-ai/kernel": "0.2.9",
38
38
  "@tanstack/vue-query": "^5.101.0",
39
- "fastify": "^5.8.5",
39
+ "fastify": "^5.12.7",
40
40
  "pino": "^10.3.1",
41
41
  "json-rest-schema": "^1.0.17",
42
42
  "pinia": "^4.0.3",
43
43
  "vue": "^3.5.44",
44
44
  "vue-router": "^5.3.1",
45
45
  "vuetify": "^4.1.2",
46
- "@jskit-ai/http-runtime": "0.2.7",
46
+ "@jskit-ai/http-runtime": "0.2.9",
47
47
  "@fastify/fast-json-stringify-compiler": "^5.1.0",
48
48
  "@vue/devtools-api": "^8.2.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@jskit-ai/config-eslint": "0.1.245",
52
- "@jskit-ai/jskit-catalog": "0.2.22",
52
+ "@jskit-ai/jskit-catalog": "0.2.24",
53
53
  "@playwright/test": "1.61.1",
54
54
  "@vitejs/plugin-vue": "^6.0.7",
55
55
  "eslint": "^10.8.0",
@@ -102,12 +102,6 @@ async function createServer({ runtimeEnv }) {
102
102
  }
103
103
  });
104
104
 
105
- app.get("/api/health", async () => {
106
- return {
107
- ok: true,
108
- app: "reading-room"
109
- };
110
- });
111
105
  const appRoot = path.resolve(process.cwd());
112
106
  const distRoot = path.resolve(appRoot, "dist");
113
107
  const hasWebBuild = existsSync(path.resolve(distRoot, SPA_INDEX_FILE));
@@ -124,6 +118,15 @@ async function createServer({ runtimeEnv }) {
124
118
  fastify: app
125
119
  });
126
120
 
121
+ // Healthy when every check the installed packages register passes, such as the database's.
122
+ app.get("/api/health", async (request, reply) => {
123
+ const health = await runtime.health.check();
124
+ return reply.code(health.ok ? 200 : 503).send({
125
+ ...health,
126
+ app: "reading-room"
127
+ });
128
+ });
129
+
127
130
  registerSurfaceRequestConstraint({
128
131
  fastify: app,
129
132
  surfaceRuntime,
@@ -3,15 +3,14 @@ import test from "node:test";
3
3
  import { createServer } from "../../server.js";
4
4
  import { resolveRuntimeEnv } from "../../server/lib/runtimeEnv.js";
5
5
 
6
- test("GET /api/health returns built-in health response", async () => {
6
+ test("GET /api/health reports the installed runtime's health checks", async (context) => {
7
7
  const app = await createServer({ runtimeEnv: resolveRuntimeEnv() });
8
+ context.after(() => app.close());
8
9
  const response = await app.inject({
9
10
  method: "GET",
10
11
  url: "/api/health"
11
12
  });
12
13
 
13
14
  assert.equal(response.statusCode, 200);
14
- assert.equal(response.json().ok, true);
15
-
16
- await app.close();
15
+ assert.deepEqual(response.json(), { ok: true, checks: {}, app: "reading-room" });
17
16
  });
@@ -34,24 +34,24 @@
34
34
  "dependencies": {
35
35
  "@local/main": "0.1.0",
36
36
  "@fastify/static": "^10.1.3",
37
- "@jskit-ai/kernel": "0.2.7",
37
+ "@jskit-ai/kernel": "0.2.9",
38
38
  "@tanstack/vue-query": "^5.101.0",
39
- "fastify": "^5.8.5",
39
+ "fastify": "^5.12.7",
40
40
  "pino": "^10.3.1",
41
41
  "json-rest-schema": "^1.0.17",
42
42
  "pinia": "^4.0.3",
43
43
  "vue": "^3.5.44",
44
44
  "vue-router": "^5.3.1",
45
45
  "vuetify": "^4.1.2",
46
- "@jskit-ai/http-runtime": "0.2.7",
46
+ "@jskit-ai/http-runtime": "0.2.9",
47
47
  "@mdi/js": "^7.4.47",
48
- "@jskit-ai/shell-web": "0.2.8",
48
+ "@jskit-ai/shell-web": "0.2.10",
49
49
  "@fastify/fast-json-stringify-compiler": "^5.1.0",
50
50
  "@vue/devtools-api": "^8.2.1"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@jskit-ai/config-eslint": "0.1.245",
54
- "@jskit-ai/jskit-catalog": "0.2.22",
54
+ "@jskit-ai/jskit-catalog": "0.2.24",
55
55
  "@playwright/test": "1.61.1",
56
56
  "@vitejs/plugin-vue": "^6.0.7",
57
57
  "eslint": "^10.8.0",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "description": "App-local runtime composition and lightweight glue.",
11
11
  "dependencies": {
12
- "@jskit-ai/kernel": "0.2.7"
12
+ "@jskit-ai/kernel": "0.2.9"
13
13
  },
14
14
  "jskit": {
15
15
  "kind": "runtime",
@@ -102,12 +102,6 @@ async function createServer({ runtimeEnv }) {
102
102
  }
103
103
  });
104
104
 
105
- app.get("/api/health", async () => {
106
- return {
107
- ok: true,
108
- app: "reading-room"
109
- };
110
- });
111
105
  const appRoot = path.resolve(process.cwd());
112
106
  const distRoot = path.resolve(appRoot, "dist");
113
107
  const hasWebBuild = existsSync(path.resolve(distRoot, SPA_INDEX_FILE));
@@ -124,6 +118,15 @@ async function createServer({ runtimeEnv }) {
124
118
  fastify: app
125
119
  });
126
120
 
121
+ // Healthy when every check the installed packages register passes, such as the database's.
122
+ app.get("/api/health", async (request, reply) => {
123
+ const health = await runtime.health.check();
124
+ return reply.code(health.ok ? 200 : 503).send({
125
+ ...health,
126
+ app: "reading-room"
127
+ });
128
+ });
129
+
127
130
  registerSurfaceRequestConstraint({
128
131
  fastify: app,
129
132
  surfaceRuntime,
@@ -3,15 +3,14 @@ import test from "node:test";
3
3
  import { createServer } from "../../server.js";
4
4
  import { resolveRuntimeEnv } from "../../server/lib/runtimeEnv.js";
5
5
 
6
- test("GET /api/health returns built-in health response", async () => {
6
+ test("GET /api/health reports the installed runtime's health checks", async (context) => {
7
7
  const app = await createServer({ runtimeEnv: resolveRuntimeEnv() });
8
+ context.after(() => app.close());
8
9
  const response = await app.inject({
9
10
  method: "GET",
10
11
  url: "/api/health"
11
12
  });
12
13
 
13
14
  assert.equal(response.statusCode, 200);
14
- assert.equal(response.json().ok, true);
15
-
16
- await app.close();
15
+ assert.deepEqual(response.json(), { ok: true, checks: {}, app: "reading-room" });
17
16
  });