@netlify/agent-runner-cli 2.16.0-server.2 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin-local.js +257 -263
- package/dist/bin.js +321 -327
- package/dist/index.js +256 -262
- package/dist/interactions-mcp-server.js +1 -1
- package/dist/skills/netlify-identity/SKILL.md +246 -568
- package/dist/skills/netlify-identity/references/advanced-patterns.md +104 -0
- package/dist/skills/netlify-identity/references/authorization-and-sessions.md +24 -0
- package/dist/skills/netlify-server@netlifyserver/SKILL.md +31 -83
- package/package.json +2 -1
- package/scripts/build-skills.js +40 -18
- package/dist/skills/general-backend@backend/SKILL.md +0 -78
- package/dist/skills/netlify-functions/SKILL@backend.md +0 -610
- package/dist/skills/netlify-server@backend/SKILL.md +0 -225
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Advanced Identity Patterns
|
|
2
|
+
|
|
3
|
+
## Password Recovery
|
|
4
|
+
|
|
5
|
+
Three-step flow: request recovery email, handle the callback, then set a new password.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { requestPasswordRecovery, handleAuthCallback, updateUser, AuthError } from '@netlify/identity'
|
|
9
|
+
|
|
10
|
+
// Step 1: Send recovery email
|
|
11
|
+
async function handleForgotPassword(email: string) {
|
|
12
|
+
try {
|
|
13
|
+
await requestPasswordRecovery(email)
|
|
14
|
+
showSuccess('Check your email for a password reset link.')
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof AuthError) showError(error.message)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Step 2: handleAuthCallback() returns { type: 'recovery', user } — show password reset form
|
|
21
|
+
// (See the handleAuthCallback switch in SKILL.md)
|
|
22
|
+
|
|
23
|
+
// Step 3: Set new password
|
|
24
|
+
async function handlePasswordReset(newPassword: string) {
|
|
25
|
+
try {
|
|
26
|
+
await updateUser({ password: newPassword })
|
|
27
|
+
showSuccess('Password updated.')
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error instanceof AuthError) showError(error.message)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The recovery callback fires a `'recovery'` auth event, not `'login'`. The user is authenticated but should be prompted to set a new password before navigating away.
|
|
35
|
+
|
|
36
|
+
## Invite Acceptance
|
|
37
|
+
|
|
38
|
+
When a user clicks an invite link, `handleAuthCallback()` returns `{ type: 'invite', user: null, token }`. Use the token to accept the invite and set a password.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { acceptInvite, AuthError } from '@netlify/identity'
|
|
42
|
+
|
|
43
|
+
async function handleAcceptInvite(token: string, password: string) {
|
|
44
|
+
try {
|
|
45
|
+
const user = await acceptInvite(token, password)
|
|
46
|
+
showSuccess(`Welcome, ${user.email}! Your account is ready.`)
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof AuthError) showError(error.message)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Email Change
|
|
54
|
+
|
|
55
|
+
When a user verifies an email change, `handleAuthCallback()` returns `{ type: 'email_change', user }` with the change already applied — the returned `user` carries the new email. The user must be logged in when clicking the verification link.
|
|
56
|
+
|
|
57
|
+
Like recovery (and unlike invite acceptance), there is no token to consume and no follow-up call — `handleAuthCallback()` does the work. Handle it in the `handleAuthCallback()` switch (see SKILL.md) and confirm the new address.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// In the handleAuthCallback() switch (see SKILL.md):
|
|
61
|
+
case 'email_change':
|
|
62
|
+
showSuccess(`Email updated to ${result.user?.email}`)
|
|
63
|
+
break
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If you process the verification token yourself instead of letting `handleAuthCallback()` read the URL hash, call `verifyEmailChange(token)` directly — it applies the change and returns the updated user.
|
|
67
|
+
|
|
68
|
+
## Session Hydration
|
|
69
|
+
|
|
70
|
+
`hydrateSession()` bridges server-set cookies to the browser session. Call it on page load when using server-side login (e.g., login inside a Netlify Function followed by a redirect).
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { hydrateSession } from '@netlify/identity'
|
|
74
|
+
|
|
75
|
+
const user = await hydrateSession()
|
|
76
|
+
if (user) {
|
|
77
|
+
// Browser session is now in sync with server-set cookies
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`getUser()` auto-hydrates from the `nf_jwt` cookie if no browser session exists, so explicit `hydrateSession()` is only needed when you want to restore the full session (including token refresh timers) after a server-side login.
|
|
82
|
+
|
|
83
|
+
## SSR Integration Patterns
|
|
84
|
+
|
|
85
|
+
For SSR frameworks, the recommended pattern is:
|
|
86
|
+
|
|
87
|
+
- **Browser-side** for auth mutations: `login()`, `signup()`, `logout()`, `oauthLogin()`
|
|
88
|
+
- **Server-side** for reading auth state: `getUser()`, `getSettings()`, `getIdentityConfig()`
|
|
89
|
+
|
|
90
|
+
Browser-side auth mutations set the `nf_jwt` cookie and localStorage, and emit `onAuthChange` events. The server reads the cookie on the next request.
|
|
91
|
+
|
|
92
|
+
The library also supports server-side mutations (`login()`, `signup()`, `logout()` inside Netlify Functions), but these require the Netlify Functions runtime to set cookies. After a server-side mutation, use a full page navigation so the browser sends the new cookie.
|
|
93
|
+
|
|
94
|
+
Always use `window.location.href` (not framework router navigation) after server-side auth mutations in Next.js, TanStack Start, and SvelteKit. Remix `redirect()` is safe because Remix actions return real HTTP responses.
|
|
95
|
+
|
|
96
|
+
## Full API Reference
|
|
97
|
+
|
|
98
|
+
For the complete API reference — all function signatures, type definitions, OAuth helpers, admin operations, session management, auth events, and framework-specific examples — read the package README:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
node_modules/@netlify/identity/README.md
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The README is shipped with the npm package and is always in sync with the installed version.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Netlify Identity — authorization and session gotchas
|
|
2
|
+
|
|
3
|
+
Where role-based access actually gets enforced, and why a role change doesn't take effect immediately.
|
|
4
|
+
|
|
5
|
+
## Admin operations run only in the Functions runtime
|
|
6
|
+
|
|
7
|
+
Identity's admin API — creating users, updating a user's roles or metadata, deleting users (the `admin.*` operations) — requires a privileged admin token that is available **only in the Netlify Functions runtime**. It is not exposed to browser code and is not available in Edge Functions. Do all role assignment and user administration from inside a modern v2 Function (or an Identity event function), never from the client and never from an edge function. A "promote this user to admin" button in the UI must call a Function endpoint that performs the change server-side — it cannot call the admin API directly from the browser.
|
|
8
|
+
|
|
9
|
+
You don't configure or read the admin token yourself — the Netlify Functions runtime provides it automatically to `admin.*` calls made inside a Function, so there is no env var to set and no `Netlify.env.get(...)` to call for it. Run the `admin.*` operations server-side in a Function; never hardcode an admin token, ship one in the client bundle, or expose the admin operations to the browser. Exposing an admin capability client-side would let any visitor grant themselves the `admin` role.
|
|
10
|
+
|
|
11
|
+
## Redirect gating only covers CDN document requests
|
|
12
|
+
|
|
13
|
+
`conditions = { Role = [...] }` redirects are enforced by the CDN **only when it serves a document (navigation) request** — a fresh HTTP request for the path. They are a coarse page-level perimeter, not real authorization:
|
|
14
|
+
|
|
15
|
+
- **SPA client-side navigation bypasses them.** When a client-side router (React, Vue, SvelteKit, etc.) navigates to `/admin` in the browser, no new document request reaches the CDN, so the redirect rule never runs and the route renders regardless of the user's role.
|
|
16
|
+
- **Anything in the client bundle is downloadable by anyone.** Role-gated content compiled into the JavaScript bundle ships to every visitor who can load the page; hiding a component behind a client-side role check does not protect the data inside it.
|
|
17
|
+
|
|
18
|
+
So use redirect gating for coarse routing only. Enforce anything sensitive **server-side on every request** — a Netlify Function (or the API it calls) that resolves the user with `getUser()` and checks the server-controlled `app_metadata.roles` — never a client-side route guard or a hidden UI element as the only gate.
|
|
19
|
+
|
|
20
|
+
## Role changes don't affect live sessions until the JWT refreshes
|
|
21
|
+
|
|
22
|
+
Roles are baked into the `nf_jwt` when the token is issued, and that JWT stays valid until it expires (about an hour). Changing a user's roles — via the dashboard, the admin API, or an Identity event function — does **not** update tokens already held by signed-in users. A user you just promoted keeps seeing the old view, and a user whose role you just revoked keeps their access, until their token refreshes (`AUTH_EVENTS.TOKEN_REFRESH`) or they log out and log back in. Both redirect `Role` conditions and function-side `app_metadata.roles` checks read the current token, so both see the stale roles until then.
|
|
23
|
+
|
|
24
|
+
Don't expect a role change to take effect mid-session. When it needs to apply right away, direct the user to log out and back in (or otherwise refresh their token) so a new `nf_jwt` carrying the updated roles is issued.
|
|
@@ -9,20 +9,17 @@ A Netlify Server is one long-running Node.js process deployed alongside your sit
|
|
|
9
9
|
|
|
10
10
|
## When to use this skill: explicit request only
|
|
11
11
|
|
|
12
|
-
Netlify Server is
|
|
12
|
+
Netlify Server is an experimental primitive. The rule for choosing it is strict:
|
|
13
13
|
|
|
14
14
|
- ONLY build with a Netlify Server when the prompt **explicitly requests a "Netlify Server"** by name (for example "build this as a Netlify Server" or "use the Netlify Server primitive").
|
|
15
15
|
- If the prompt does not name it, discard this skill entirely, even when the request mentions Express, Hono, WebSockets, servers in general, or would otherwise seem like a good fit. In those cases use **Netlify Functions** (see the `netlify-functions` skill) and the other platform primitives as usual.
|
|
16
16
|
- NEVER migrate an existing site's functions to a Netlify Server unless explicitly asked to.
|
|
17
17
|
|
|
18
|
-
Once a Netlify Server has been explicitly requested:
|
|
19
|
-
|
|
20
|
-
- Start without a template. Every template is a full-stack framework whose catch-all function takes the requests before the server, so the server would never receive traffic.
|
|
21
|
-
- Implement all API routes inside the server app. Create files in `netlify/functions/` only for scheduled jobs (see below), never for endpoints.
|
|
18
|
+
Once a Netlify Server has been explicitly requested: implement all API routes inside the server app, and do NOT create files in `netlify/functions/` alongside it.
|
|
22
19
|
|
|
23
20
|
## Quick start
|
|
24
21
|
|
|
25
|
-
Create exactly one entrypoint at `netlify/server/index.mjs`.
|
|
22
|
+
Create exactly one entrypoint at `netlify/server/index.mjs`. Any Node.js server works. It MUST listen on `process.env.PORT`.
|
|
26
23
|
|
|
27
24
|
Express:
|
|
28
25
|
|
|
@@ -52,87 +49,49 @@ app.get('/api/hello', (c) => c.json({ hello: 'world' }))
|
|
|
52
49
|
export default app
|
|
53
50
|
```
|
|
54
51
|
|
|
55
|
-
What the entry file exports decides how Netlify runs it:
|
|
56
|
-
|
|
57
|
-
- **An `http.Server`**, such as `http.createServer(app)`: requests go to that server. WebSocket libraries attached to it (`ws`, Socket.IO) work.
|
|
58
|
-
- **An object with a `fetch` method**, such as a Hono app: called for each request.
|
|
59
|
-
- **An Express app**, or a function with 3 or more parameters: hosted in an HTTP server Netlify creates. WebSocket upgrades get a `501`, so export `http.createServer(app)` instead when you need them.
|
|
60
|
-
- **Any other function**: called as a handler `(request, context) => Response`, the same signature as a Netlify Function. A plain Node.js `(req, res)` listener lands here too and breaks; wrap it in `http.createServer()`.
|
|
61
|
-
- **Anything else, or no default export**: Netlify waits for your code to start a server with `listen` on `process.env.PORT`, the way the framework's documentation shows. It must be listening within 10 seconds.
|
|
62
|
-
|
|
63
|
-
A handler or `fetch` app accepts WebSockets with `upgradeWebSocket(request)` from `@netlify/server`, which returns `{ socket, response }`; return `response` from the handler.
|
|
64
|
-
|
|
65
52
|
Declare dependencies in `package.json` as usual. Deploy is the normal site deploy; nothing extra to configure. The build log confirms detection with the line `Netlify Server detected at netlify/server/index.mjs`.
|
|
66
53
|
|
|
67
54
|
## Rules
|
|
68
55
|
|
|
69
56
|
- **One entrypoint only.** `netlify/server/` must contain exactly one of `index.mjs`, `index.js`, `index.ts`, or `index.mts`. Two entry files fail the build. Prefer `index.mjs` with ESM syntax; split the rest of the server into modules that the entrypoint imports.
|
|
70
|
-
- **
|
|
71
|
-
- **Node.js 24 or later.** The server runs on the build's Node.js version, and the deploy fails on anything earlier.
|
|
57
|
+
- **Listen on `process.env.PORT`.** It is set before your code is imported. Do not hardcode a port.
|
|
72
58
|
- **Do not daemonize, fork, or spawn long-lived child processes.** One process serves the traffic.
|
|
73
59
|
- Environment variables work as usual via `process.env`.
|
|
74
60
|
- When adding npm modules, ensure `node_modules` is in `.gitignore`.
|
|
75
61
|
|
|
76
62
|
## Routing and static files
|
|
77
63
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
1. Edge Functions.
|
|
81
|
-
2. Redirect and rewrite rules.
|
|
82
|
-
3. Static files in the publish directory.
|
|
83
|
-
4. Netlify Functions, on the paths they're configured for.
|
|
84
|
-
5. The server, for everything else (or only the paths it's configured for, see below).
|
|
64
|
+
The server receives every request that is not served as a static file:
|
|
85
65
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
- Files in the publish directory win over the server for matching paths, even on paths the server claims. Use the publish directory for assets like images, fonts, or a favicon.
|
|
66
|
+
- Files in the publish directory win over the server for matching paths (`preferStatic`). Use the publish directory for assets like images, fonts, or a favicon.
|
|
89
67
|
- Do NOT put an `index.html` in the publish directory if the server should render `/`. A static `index.html` shadows the server's root route.
|
|
90
|
-
-
|
|
91
|
-
- Full-stack frameworks (Next.js, Nuxt, SvelteKit, Astro, TanStack Start) usually deploy a Netlify Function on `/*` that handles every request not served by a static file, covering both page rendering and the framework's API routes. Like the rewrite above, it takes those requests before the server does, so the server never receives any. If a Netlify Server is explicitly required, do not use a full-stack framework. Otherwise, put server code in the framework's own server routes.
|
|
92
|
-
- There is no path prefix: the server sees its requests with their original paths.
|
|
93
|
-
|
|
94
|
-
To have the server handle only some paths, set `path` (defaults to `/*`) and `excludedPath`, using the same syntax as function paths. Export them as `config` from the entry file, or set them in a `[server]` section in `netlify.toml`:
|
|
95
|
-
|
|
96
|
-
```javascript
|
|
97
|
-
export const config = {
|
|
98
|
-
path: '/api/*',
|
|
99
|
-
excludedPath: ['/api/legacy/*'],
|
|
100
|
-
}
|
|
101
|
-
```
|
|
68
|
+
- There is no path prefix: the server sees `/`, `/api/anything`, and everything else, with the original paths.
|
|
102
69
|
|
|
103
70
|
## Lifecycle: scale to zero
|
|
104
71
|
|
|
105
72
|
This is the most important mental model for building correctly:
|
|
106
73
|
|
|
107
|
-
- After
|
|
74
|
+
- After a period with no traffic, the server is shut down. The next request starts a fresh process (a cold start).
|
|
108
75
|
- **All process memory is lost on every scale-to-zero, deploy, and platform maintenance event.** In-memory caches are fine as caches; they are NEVER storage. Anything that must survive belongs in Netlify Blobs or Netlify Database (see the `general-database` skill to choose).
|
|
109
|
-
-
|
|
110
|
-
- In-flight requests and open WebSocket or Server-Sent Events connections are never cut off by the idle shutdown; while any are open, the instance stays running.
|
|
111
|
-
- The server must send a response's status and headers within 20 seconds, or the request fails with a `502`. Stream slow responses, such as AI output, or respond right away and finish with `waitUntil`.
|
|
76
|
+
- In-flight requests are never cut off by the idle shutdown.
|
|
112
77
|
- Background work that outlives the request that triggered it (for example, responding early and then finishing a task) MUST be registered with `getContext().waitUntil(promise)`. Registered work keeps the process alive until the promise settles; unregistered work may be reclaimed the moment no requests are active.
|
|
113
|
-
- Never keep the server alive on purpose: no self-pings, no timer that calls the server, no `waitUntil` chain that schedules more work to stay running, and no `scale.min` above `0` (during Early Access a deploy that sets it fails). Scale to zero is the design. If the user asks for an always-on server, explain this and build without it.
|
|
114
|
-
- Scheduled or recurring work (cron, nightly cleanup, periodic sync) goes in a scheduled function (see the `netlify-functions` skill) that calls a server route. Never use `setInterval` or other timers meant to outlive a request.
|
|
115
78
|
- `waitUntil` extends the idle lifetime, but deploys and platform maintenance still stop the process with only a short grace window. Keep background tasks short (seconds, not minutes), and persist anything critical before responding. For genuinely long jobs, persist a job record and process it in resumable increments across requests.
|
|
116
79
|
|
|
117
|
-
Graceful shutdown hook (optional):
|
|
80
|
+
Graceful shutdown hook (optional): export a `shutdown` function from the entrypoint; it is called before the process is stopped. Keep it fast (a few seconds), for example flushing a buffer to Blobs. For Express-style apps that call `listen`, you can also listen for `SIGTERM` directly.
|
|
118
81
|
|
|
119
82
|
```javascript
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
wss.close()
|
|
124
|
-
await drained
|
|
125
|
-
await pool.end()
|
|
126
|
-
})
|
|
83
|
+
export const shutdown = async () => {
|
|
84
|
+
await flushPendingWrites()
|
|
85
|
+
}
|
|
127
86
|
```
|
|
128
87
|
|
|
129
88
|
## Request context: geolocation, cookies, site metadata
|
|
130
89
|
|
|
131
|
-
Inside request handling, get the Netlify request context with `getContext` from the `@netlify/
|
|
90
|
+
Inside request handling, get the Netlify request context with `getContext` from the `@netlify/functions` package (add it to `package.json`):
|
|
132
91
|
|
|
133
92
|
```javascript
|
|
134
93
|
import express from 'express'
|
|
135
|
-
import { getContext } from '@netlify/
|
|
94
|
+
import { getContext } from '@netlify/functions'
|
|
136
95
|
|
|
137
96
|
const app = express()
|
|
138
97
|
|
|
@@ -148,7 +107,6 @@ Rules:
|
|
|
148
107
|
|
|
149
108
|
- ONLY call `getContext()` while handling a request (inside a route handler, middleware, or code awaited from one). It throws outside a request.
|
|
150
109
|
- Works with every server form, including WebSocket upgrade handling.
|
|
151
|
-
- Anything started inside a request (a `setInterval`, a lazily started background loop, a client that creates its own timers, WebSocket message handlers) keeps that request's context for its whole life, so `getContext()` there returns a stale request instead of throwing. Start long-lived work outside request handling, and never read per-request context from it.
|
|
152
110
|
|
|
153
111
|
### SSO user: `context.user`
|
|
154
112
|
|
|
@@ -168,62 +126,52 @@ See the [changelog post](https://www.netlify.com/changelog/2026-10-07-context-us
|
|
|
168
126
|
|
|
169
127
|
The AI Gateway works with zero configuration: provider credentials and base URLs (`OPENAI_API_KEY`, `OPENAI_BASE_URL`, and equivalents for other providers) are injected into `process.env` automatically. See the `netlify-ai-gateway` skill for supported models and usage.
|
|
170
128
|
|
|
171
|
-
|
|
129
|
+
One rule specific to servers: **construct AI SDK clients inside request handling, never at module scope.** Credentials are injected per request, so they do not exist when the module is first imported.
|
|
172
130
|
|
|
173
131
|
```javascript
|
|
174
132
|
import OpenAI from 'openai'
|
|
175
133
|
|
|
176
|
-
const client = new OpenAI()
|
|
177
|
-
|
|
178
134
|
app.post('/api/chat', async (req, res) => {
|
|
135
|
+
const client = new OpenAI() // created per request: credentials are available here
|
|
179
136
|
// ...
|
|
180
137
|
})
|
|
181
138
|
```
|
|
182
139
|
|
|
183
140
|
## WebSockets
|
|
184
141
|
|
|
185
|
-
WebSockets work for servers that handle HTTP upgrades
|
|
142
|
+
WebSockets work for servers that handle HTTP upgrades (the `listen` form). Attach the WebSocket server to the same HTTP server:
|
|
186
143
|
|
|
187
144
|
```javascript
|
|
188
145
|
// netlify/server/index.mjs
|
|
189
|
-
import { createServer } from 'node:http'
|
|
190
146
|
import express from 'express'
|
|
191
147
|
import { WebSocketServer } from 'ws'
|
|
192
148
|
|
|
193
149
|
const app = express()
|
|
194
|
-
const server =
|
|
195
|
-
const wss = new WebSocketServer({
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
150
|
+
const server = app.listen(process.env.PORT)
|
|
151
|
+
const wss = new WebSocketServer({ noServer: true })
|
|
152
|
+
|
|
153
|
+
server.on('upgrade', (req, socket, head) => {
|
|
154
|
+
if (req.url !== '/ws') {
|
|
155
|
+
socket.destroy()
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
159
|
+
ws.send('connected')
|
|
160
|
+
})
|
|
199
161
|
})
|
|
200
|
-
|
|
201
|
-
export default server
|
|
202
162
|
```
|
|
203
163
|
|
|
204
164
|
Rules for WebSocket clients you generate:
|
|
205
165
|
|
|
206
166
|
- ALWAYS implement automatic reconnection with backoff in the browser client. The platform may terminate long-lived connections periodically; a reconnecting client makes this invisible to users.
|
|
207
167
|
- Do not rely on a socket staying open indefinitely, and do not keep client state only on the server side of a socket: rebuild the needed state on reconnect.
|
|
208
|
-
- Clients can be connected to different instances, and each instance only sees its own sockets. Keep state every client must see, such as chat history, in Netlify Database or Blobs.
|
|
209
|
-
|
|
210
|
-
## Fetching user-supplied URLs
|
|
211
|
-
|
|
212
|
-
An endpoint that fetches a URL taken from the request (link previews, webhook testers, image proxies) runs inside Netlify's network, so it must:
|
|
213
|
-
|
|
214
|
-
- Accept only `http:` and `https:` URLs.
|
|
215
|
-
- Resolve the hostname and refuse loopback, private, link-local, and unique-local addresses, including `169.254.169.254`.
|
|
216
|
-
- Follow redirects manually (`redirect: 'manual'`) and check every hop the same way.
|
|
217
|
-
- Return only what the feature needs (extracted fields, a capped body), never forward the caller's method, headers, or body. A generic `/proxy?url=` endpoint is an open relay; don't build one.
|
|
218
168
|
|
|
219
169
|
## Common mistakes
|
|
220
170
|
|
|
221
171
|
- Treating process memory as a database. It is wiped on every cold start. Use Blobs or Database.
|
|
222
172
|
- Hardcoding a port or defaulting to `3000` without reading `process.env.PORT` first.
|
|
223
173
|
- Adding an `index.html` to the publish directory that shadows the server's `/` route.
|
|
224
|
-
- Creating `netlify/functions/` endpoints alongside the server. Put API routes in the server app
|
|
174
|
+
- Creating `netlify/functions/` endpoints alongside the server. Put API routes in the server app.
|
|
225
175
|
- Two entry files in `netlify/server/` (for example both `index.mjs` and `index.ts`). Keep one.
|
|
226
176
|
- Fire-and-forget background jobs without `getContext().waitUntil(promise)`. Unregistered work may be reclaimed mid-flight; registered work should still be short and non-critical.
|
|
227
|
-
-
|
|
228
|
-
- Storing `NETLIFY_PURGE_API_TOKEN` in a module-level variable. Read it when you call the purge API.
|
|
229
|
-
- Keeping the server warm with self-pings or timers, or running scheduled work with `setInterval`. Use a scheduled function that calls a server route.
|
|
177
|
+
- Constructing AI SDK clients (or calling `getContext()`) at module scope. Per-request state does not exist at import time.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netlify/agent-runner-cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.16.0
|
|
4
|
+
"version": "2.16.0",
|
|
5
5
|
"description": "CLI tool for running Netlify agents",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"test:integration:claude": "vitest run test/integration/claude.test.ts",
|
|
44
44
|
"test:integration:gemini": "vitest run test/integration/gemini.test.ts",
|
|
45
45
|
"test:integration:opencode": "vitest run test/integration/opencode.test.ts",
|
|
46
|
+
"test:integration:antigravity": "vitest run test/integration/antigravity.test.ts",
|
|
46
47
|
"test:integration:muse": "vitest run test/integration/muse.test.ts",
|
|
47
48
|
"test:integration:skill-invocation": "vitest run test/integration/skill-invocation.test.ts",
|
|
48
49
|
"check:types": "tsc --noEmit",
|
package/scripts/build-skills.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { cp, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
2
3
|
import { createRequire } from 'node:module'
|
|
3
4
|
import path from 'node:path'
|
|
4
5
|
import process from 'node:process'
|
|
@@ -8,34 +9,55 @@ const localSkillsDir = new URL('../src/skills/', import.meta.url)
|
|
|
8
9
|
const sharedPackagePath = createRequire(import.meta.url).resolve('@netlify/skills/package.json')
|
|
9
10
|
const sharedSkillsDir = path.join(path.dirname(sharedPackagePath), 'skills')
|
|
10
11
|
export const bundledSkillsDir = fileURLToPath(new URL('../dist/skills/', import.meta.url))
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
const skillOverridesDir = fileURLToPath(new URL('../src/skill-overrides/', import.meta.url))
|
|
13
|
+
export const selectedSharedSkills = ['netlify-access-control', 'netlify-forms', 'netlify-image-cdn', 'netlify-identity']
|
|
14
|
+
|
|
15
|
+
const readOptionalFile = async (filePath) => {
|
|
16
|
+
try {
|
|
17
|
+
return await readFile(filePath, 'utf8')
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT') return undefined
|
|
20
|
+
throw error
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const applySkillReplacements = (content, replacements) => {
|
|
25
|
+
let result = content
|
|
26
|
+
for (const { from, to } of replacements) {
|
|
27
|
+
if (!from || result.split(from).length !== 2) {
|
|
28
|
+
throw new Error(`Expected exactly one occurrence of shared skill text: ${JSON.stringify(from)}`)
|
|
29
|
+
}
|
|
30
|
+
result = result.replace(from, () => to)
|
|
31
|
+
}
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
23
34
|
|
|
24
35
|
export const buildSkills = async (targetDir = bundledSkillsDir) => {
|
|
25
36
|
await rm(targetDir, { recursive: true, force: true })
|
|
26
37
|
await cp(localSkillsDir, targetDir, { recursive: true })
|
|
27
38
|
|
|
28
|
-
for (const
|
|
39
|
+
for (const name of selectedSharedSkills) {
|
|
29
40
|
await cp(path.join(sharedSkillsDir, name), path.join(targetDir, name), {
|
|
30
41
|
recursive: true,
|
|
31
42
|
force: false,
|
|
32
43
|
errorOnExist: true,
|
|
33
44
|
})
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
const overrideDir = path.join(skillOverridesDir, name)
|
|
46
|
+
const replacements = await readOptionalFile(path.join(overrideDir, 'replacements.json'))
|
|
47
|
+
const instructions = await readOptionalFile(path.join(overrideDir, 'instructions.md'))
|
|
48
|
+
if (instructions !== undefined || replacements !== undefined) {
|
|
49
|
+
const skillPath = path.join(targetDir, name, 'SKILL.md')
|
|
50
|
+
let content = await readFile(skillPath, 'utf8')
|
|
51
|
+
if (replacements !== undefined) {
|
|
52
|
+
content = applySkillReplacements(content, JSON.parse(replacements))
|
|
53
|
+
}
|
|
54
|
+
if (instructions !== undefined) {
|
|
55
|
+
content += `\n\n${instructions}`
|
|
56
|
+
}
|
|
57
|
+
await writeFile(skillPath, content)
|
|
36
58
|
}
|
|
37
|
-
|
|
38
|
-
|
|
59
|
+
const assetsDir = path.join(overrideDir, 'assets')
|
|
60
|
+
if (existsSync(assetsDir)) {
|
|
39
61
|
for (const entry of await readdir(assetsDir, { recursive: true, withFileTypes: true })) {
|
|
40
62
|
if (!entry.isFile()) continue
|
|
41
63
|
const source = path.join(entry.parentPath, entry.name)
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: general-backend
|
|
3
|
-
description: Use this skill whenever the task involves server-side code — API endpoints, backend logic, request handlers, WebSockets, server-side AI calls, auth checks, or any code that must run on a server rather than in the browser. Covers Netlify Server, Netlify Functions, Edge Functions, and full-stack framework server routes.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Server-side code on Netlify
|
|
7
|
-
|
|
8
|
-
Where server-side code goes depends on the project. Work out which case applies before writing code, and for a
|
|
9
|
-
new project, before scaffolding a template.
|
|
10
|
-
|
|
11
|
-
## New project that needs a Netlify Server: no template
|
|
12
|
-
|
|
13
|
-
The create-mode templates are built on a full-stack framework that deploys its server-side rendering as a Netlify
|
|
14
|
-
Function on every path, so a Netlify Server added to a template project never receives requests. If the project
|
|
15
|
-
needs a Netlify Server, don't scaffold a template. A new project needs one when it has any request-driven backend
|
|
16
|
-
code: API endpoints, data saved and read through the backend, WebSockets, server-side AI calls, or an HTTP server
|
|
17
|
-
framework such as Express, Hono, or Fastify.
|
|
18
|
-
|
|
19
|
-
A site with no backend code (static pages, a marketing or content site, a client-only tool) needs neither a server
|
|
20
|
-
nor functions. Use the templates as usual for it.
|
|
21
|
-
|
|
22
|
-
## Project with a full-stack framework: use its server routes
|
|
23
|
-
|
|
24
|
-
Full-stack frameworks (such as TanStack Start, Next.js, Astro, SvelteKit, Remix, Nuxt) build
|
|
25
|
-
their own server code into Netlify Functions at deploy time. In these projects, put request handling in the
|
|
26
|
-
framework's server routes or server functions instead of adding a Netlify Server. Call Netlify Database, Blobs, and
|
|
27
|
-
the AI Gateway from those routes. Use Netlify Functions alongside them only for the cases listed under "Only when
|
|
28
|
-
Server can't" below.
|
|
29
|
-
|
|
30
|
-
HTTP server frameworks such as Express, Hono, Fastify, and Koa are different: nothing deploys them for you, so they
|
|
31
|
-
run as a Netlify Server, covered next.
|
|
32
|
-
|
|
33
|
-
## Otherwise: Netlify Server
|
|
34
|
-
|
|
35
|
-
**Netlify Server** runs one long-running Node.js process (Express, Hono, Fastify, or plain `node:http`) from
|
|
36
|
-
`netlify/server/index.mjs`. Put every request-driven piece of backend code in it:
|
|
37
|
-
|
|
38
|
-
- API endpoints and route handlers
|
|
39
|
-
- Server-rendered pages and form handling in your own routes
|
|
40
|
-
- WebSockets and streaming responses
|
|
41
|
-
- Calls to the Netlify AI Gateway, Netlify Database, and Netlify Blobs
|
|
42
|
-
- Short work that continues after the response (seconds, such as sending an email), registered with `waitUntil`
|
|
43
|
-
- Auth checks, webhooks from third-party services, proxies to external APIs
|
|
44
|
-
|
|
45
|
-
Don't pick Functions just because the task is small or looks like a single endpoint.
|
|
46
|
-
|
|
47
|
-
See the **netlify-server** skill for complete setup instructions and usage examples.
|
|
48
|
-
|
|
49
|
-
## Only when Server can't: Netlify Functions
|
|
50
|
-
|
|
51
|
-
Use **Netlify Functions** alongside the server only for:
|
|
52
|
-
|
|
53
|
-
- **Scheduled functions**: work that runs on a cron schedule, with no incoming request.
|
|
54
|
-
- **Event handlers**: code triggered by platform events, such as Netlify Forms submissions, Netlify Identity events
|
|
55
|
-
(signup, login, validate), or deploy events.
|
|
56
|
-
- **Background functions**: jobs that can run longer than about a minute and must not be cut off midway, such as
|
|
57
|
-
generating a report or processing an upload (up to 15 minutes). When a server instance is replaced by a deploy or
|
|
58
|
-
scaling event, its background work gets only 90 seconds to finish. The server enqueues the job by calling the
|
|
59
|
-
background function and returns right away.
|
|
60
|
-
|
|
61
|
-
Scheduled functions and event handlers run without an incoming request, which a server can't do, and background
|
|
62
|
-
functions aren't cut off when a server instance is replaced. Everything else stays in the server, including the
|
|
63
|
-
endpoints these functions talk to.
|
|
64
|
-
|
|
65
|
-
See the **netlify-functions** skill for complete setup instructions and usage examples.
|
|
66
|
-
|
|
67
|
-
## Request and response changes at the edge: Edge Functions
|
|
68
|
-
|
|
69
|
-
Use **Edge Functions** for logic that runs on every request before it reaches the server or the site: rewrites and
|
|
70
|
-
redirects based on geolocation or cookies, A/B tests, header changes, or lightweight auth checks. They run in Deno
|
|
71
|
-
close to visitors and don't replace the server for application logic.
|
|
72
|
-
|
|
73
|
-
See the **netlify-edge-functions** skill for setup instructions.
|
|
74
|
-
|
|
75
|
-
## Existing projects
|
|
76
|
-
|
|
77
|
-
If an existing project already serves its backend from `netlify/functions/`, keep using that for changes to it.
|
|
78
|
-
Don't migrate an existing backend to a Netlify Server unless asked.
|