@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.
- package/guide/agent/app-extras/assistant-conversation.md +3 -2
- package/guide/agent/app-extras/assistant.md +35 -0
- package/guide/agent/app-setup/authentication.md +149 -0
- package/guide/agent/app-setup/database-layer.md +4 -0
- package/guide/agent/app-setup/initial-scaffolding.md +76 -0
- package/guide/agent/framework/typed-features.md +5 -0
- package/package.json +1 -1
- package/patterns/feature-package/example/booking-engine/package.json +1 -1
- package/patterns/minimal-foundation/PATTERN.md +3 -1
- package/patterns/minimal-foundation/example/package.json +4 -4
- package/patterns/minimal-foundation/example/server.js +9 -6
- package/patterns/minimal-foundation/example/tests/server/smoke.test.js +3 -4
- package/patterns/shell-foundation/example/package.json +5 -5
- package/patterns/shell-foundation/example/packages/main/package.json +1 -1
- package/patterns/shell-foundation/example/server.js +9 -6
- package/patterns/shell-foundation/example/tests/server/smoke.test.js +3 -4
- package/reference/autogen/PATTERN_INDEX.md +37 -37
- package/reference/autogen/packages/agent.md +1 -1
- package/reference/autogen/packages/assistant-runtime.md +1 -0
- package/reference/autogen/packages/auth-core.md +9 -2
- package/reference/autogen/packages/auth-provider-local-core.md +85 -21
- package/reference/autogen/packages/auth-provider-local-db-core.md +7 -0
- package/reference/autogen/packages/auth-web.md +13 -2
- package/reference/autogen/packages/crud-core.md +3 -2
- package/reference/autogen/packages/kernel.md +39 -1
- package/skills/jskit/references/pattern-index.md +37 -37
- package/skills/jskit/references/patterns/app/minimal-foundation/PATTERN.md +3 -1
- package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
- package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +9 -6
- package/skills/jskit/references/patterns/app/minimal-foundation/example/tests/server/smoke.test.js +3 -4
- package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
- package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
- package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +9 -6
- package/skills/jskit/references/patterns/app/shell-foundation/example/tests/server/smoke.test.js +3 -4
- package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -0
- package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
- package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
- package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
- package/skills/jskit/references/patterns/crud/json-api-resource-package/PATTERN.md +11 -0
- package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +2 -2
- package/skills/jskit/references/patterns/database/mysql-application/example/package.json +1 -1
- package/skills/jskit/references/patterns/database/postgres-application/example/package.json +1 -1
- package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
- package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +2 -2
- package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +3 -3
- 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
|
|
2477
|
-
|
|
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
|
@@ -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.
|
|
37
|
+
"@jskit-ai/kernel": "0.2.9",
|
|
38
38
|
"@tanstack/vue-query": "^5.101.0",
|
|
39
|
-
"fastify": "^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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
37
|
+
"@jskit-ai/kernel": "0.2.9",
|
|
38
38
|
"@tanstack/vue-query": "^5.101.0",
|
|
39
|
-
"fastify": "^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.
|
|
46
|
+
"@jskit-ai/http-runtime": "0.2.9",
|
|
47
47
|
"@mdi/js": "^7.4.47",
|
|
48
|
-
"@jskit-ai/shell-web": "0.2.
|
|
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.
|
|
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",
|
|
@@ -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
|
|
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.
|
|
15
|
-
|
|
16
|
-
await app.close();
|
|
15
|
+
assert.deepEqual(response.json(), { ok: true, checks: {}, app: "reading-room" });
|
|
17
16
|
});
|