@netlify/agent-runner-cli 1.29.1 → 1.30.1-broken.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.
@@ -0,0 +1,632 @@
1
+ ---
2
+ name: netlify-identity
3
+ description: Use whenever the task involves authentication, signups, logins, password recovery, user accounts, role-based access control, OAuth providers, protected routes, protecting Netlify Functions, server-side auth in SSR frameworks, or Netlify Identity. Always use `@netlify/identity`. Never use `netlify-identity-widget` or `gotrue-js` as they are deprecated. Make sure to always run `node scripts/enable.cjs` located in this skill's directory after implementing Netlify Identity.
4
+ ---
5
+
6
+ # Netlify Identity
7
+
8
+ Netlify Identity is a user management service for signups, logins, password recovery, user metadata, and role-based
9
+ access control. It is built on [GoTrue](https://github.com/netlify/gotrue) and issues JSON Web Tokens (JWTs).
10
+
11
+ ## Enable Feature
12
+
13
+ After implementing Netlify Identity, you **must** run the enable script to activate the feature on the deploy. The script
14
+ is located in this skill's `scripts/` directory:
15
+
16
+ ```bash
17
+ node scripts/enable.cjs
18
+ ```
19
+
20
+ **NEVER use `netlify-identity-widget` or `gotrue-js`. Always use `@netlify/identity`.** It provides a unified, headless
21
+ TypeScript API that works in both browser and server contexts (Netlify Functions, Edge Functions, SSR frameworks). It
22
+ replaces all previous Identity client libraries.
23
+
24
+ ## Setup
25
+
26
+ ```bash
27
+ npm install @netlify/identity
28
+ ```
29
+
30
+ Identity is enabled on the site when you run the enable script above. Default settings:
31
+
32
+ - **Registration** - Open (anyone can sign up). Change to Invite only in **Project configuration > Identity** if needed.
33
+ - **Autoconfirm** - Off (new signups require email confirmation). Enable in **Project configuration > Identity** to skip
34
+ confirmation during development.
35
+
36
+ For local development, use `netlify dev` so the Identity endpoint is available.
37
+
38
+ ## Quick Start
39
+
40
+ Log in from the browser:
41
+
42
+ ```typescript
43
+ import { login, getUser } from '@netlify/identity'
44
+
45
+ const user = await login('jane@example.com', 'password123')
46
+ console.log(`Hello, ${user.name}`)
47
+
48
+ // Later, check auth state
49
+ const currentUser = await getUser()
50
+ ```
51
+
52
+ Protect a Netlify Function:
53
+
54
+ ```typescript
55
+ // netlify/functions/protected.mts
56
+ import { getUser } from '@netlify/identity'
57
+ import type { Context } from '@netlify/functions'
58
+
59
+ export default async (req: Request, context: Context) => {
60
+ const user = await getUser()
61
+ if (!user) return new Response('Unauthorized', { status: 401 })
62
+ return Response.json({ id: user.id, email: user.email })
63
+ }
64
+ ```
65
+
66
+ ## Error Handling
67
+
68
+ `@netlify/identity` throws two error classes:
69
+
70
+ - **`AuthError`** - Thrown by auth operations (login, signup, logout, etc.). Has `message`, optional `status` (HTTP
71
+ status code from GoTrue), and optional `cause` (original error).
72
+ - **`MissingIdentityError`** - Thrown when Identity is not configured in the current environment (site doesn't have
73
+ Identity enabled, or not running via `netlify dev`).
74
+
75
+ `getUser()` and `isAuthenticated()` never throw - they return `null` and `false` respectively on failure.
76
+
77
+ ### Try/Catch Pattern
78
+
79
+ ```typescript
80
+ import { login, AuthError, MissingIdentityError } from '@netlify/identity'
81
+
82
+ try {
83
+ const user = await login(email, password)
84
+ } catch (error) {
85
+ if (error instanceof MissingIdentityError) {
86
+ // Identity not configured - show setup instructions
87
+ showError('Identity is not enabled on this site.')
88
+ return
89
+ }
90
+ if (error instanceof AuthError) {
91
+ switch (error.status) {
92
+ case 401:
93
+ showError('Invalid email or password.')
94
+ break
95
+ case 403:
96
+ showError('Signups are not allowed for this site.')
97
+ break
98
+ case 422:
99
+ showError('Invalid input. Check your email and password.')
100
+ break
101
+ case 404:
102
+ showError('User not found.')
103
+ break
104
+ default:
105
+ showError(error.message)
106
+ }
107
+ return
108
+ }
109
+ throw error
110
+ }
111
+ ```
112
+
113
+ ### Common Status Codes
114
+
115
+ | Status | Meaning |
116
+ |--------|---------|
117
+ | 401 | Invalid credentials or expired token |
118
+ | 403 | Action not allowed (e.g., signups disabled) |
119
+ | 422 | Validation error (e.g., weak password, malformed email) |
120
+ | 404 | User or resource not found |
121
+
122
+ ## Authentication Flows
123
+
124
+ ### Login
125
+
126
+ ```typescript
127
+ import { login, AuthError } from '@netlify/identity'
128
+
129
+ let loading = false
130
+
131
+ async function handleLogin(email: string, password: string) {
132
+ loading = true
133
+ try {
134
+ const user = await login(email, password)
135
+ showSuccess(`Welcome back, ${user.name ?? user.email}`)
136
+ } catch (error) {
137
+ if (error instanceof AuthError) {
138
+ showError(error.status === 401 ? 'Invalid email or password.' : error.message)
139
+ }
140
+ } finally {
141
+ loading = false
142
+ }
143
+ }
144
+ ```
145
+
146
+ ### Signup
147
+
148
+ After signup, check `user.emailVerified` to determine if the user was auto-confirmed (logged in immediately) or needs
149
+ to confirm their email first.
150
+
151
+ ```typescript
152
+ import { signup, AuthError } from '@netlify/identity'
153
+
154
+ async function handleSignup(email: string, password: string, name: string) {
155
+ try {
156
+ const user = await signup(email, password, { full_name: name })
157
+ if (user.emailVerified) {
158
+ // Autoconfirm is ON - user is logged in
159
+ showSuccess('Account created. You are now logged in.')
160
+ } else {
161
+ // Autoconfirm is OFF - confirmation email sent, user is NOT logged in
162
+ showSuccess('Check your email to confirm your account.')
163
+ }
164
+ } catch (error) {
165
+ if (error instanceof AuthError) {
166
+ showError(error.status === 403 ? 'Signups are not allowed.' : error.message)
167
+ }
168
+ }
169
+ }
170
+ ```
171
+
172
+ When autoconfirm is off, the confirmation email contains a link that redirects the user back to the site with
173
+ `#confirmation_token=<token>` in the URL hash. `handleAuthCallback()` processes this automatically - it calls
174
+ `confirmEmail()` under the hood, logs the user in, and returns `{ type: 'confirmation', user }`. This is why
175
+ `handleAuthCallback()` must be called on page load (see the OAuth section below for the full switch).
176
+
177
+ ### OAuth
178
+
179
+ OAuth is a two-step flow: `oauthLogin(provider)` redirects away from the site, then `handleAuthCallback()` processes
180
+ the redirect when the user returns.
181
+
182
+ ```typescript
183
+ import { oauthLogin } from '@netlify/identity'
184
+
185
+ // Step 1: Redirect to OAuth provider (this navigates away - never returns)
186
+ function handleOAuthClick(provider: 'google' | 'github' | 'gitlab' | 'bitbucket') {
187
+ oauthLogin(provider)
188
+ }
189
+ ```
190
+
191
+ ```typescript
192
+ import { handleAuthCallback, AuthError } from '@netlify/identity'
193
+
194
+ // Step 2: Process the redirect on page load
195
+ async function processCallback() {
196
+ try {
197
+ const result = await handleAuthCallback()
198
+ if (!result) return // No callback hash present - normal page load
199
+
200
+ switch (result.type) {
201
+ case 'oauth':
202
+ showSuccess(`Logged in as ${result.user?.email}`)
203
+ break
204
+ case 'confirmation':
205
+ showSuccess('Email confirmed. You are now logged in.')
206
+ break
207
+ case 'recovery':
208
+ // User is authenticated but must set a new password
209
+ showPasswordResetForm(result.user)
210
+ break
211
+ case 'invite':
212
+ // User must set a password to accept the invite
213
+ showInviteAcceptForm(result.token)
214
+ break
215
+ case 'email_change':
216
+ showSuccess('Email address updated.')
217
+ break
218
+ }
219
+ } catch (error) {
220
+ if (error instanceof AuthError) {
221
+ showError(error.message)
222
+ }
223
+ }
224
+ }
225
+ ```
226
+
227
+ Always call `handleAuthCallback()` on page load in any app that uses OAuth, password recovery, invites, or email
228
+ confirmation. It handles all callback types via the URL hash.
229
+
230
+ ### Password Recovery
231
+
232
+ Three-step flow: request recovery email, handle the callback, then set a new password.
233
+
234
+ ```typescript
235
+ import { requestPasswordRecovery, handleAuthCallback, updateUser, AuthError } from '@netlify/identity'
236
+
237
+ // Step 1: Send recovery email
238
+ async function handleForgotPassword(email: string) {
239
+ try {
240
+ await requestPasswordRecovery(email)
241
+ showSuccess('Check your email for a password reset link.')
242
+ } catch (error) {
243
+ if (error instanceof AuthError) showError(error.message)
244
+ }
245
+ }
246
+
247
+ // Step 2: handleAuthCallback() returns { type: 'recovery', user } - show password reset form
248
+ // (See the handleAuthCallback switch above)
249
+
250
+ // Step 3: Set new password
251
+ async function handlePasswordReset(newPassword: string) {
252
+ try {
253
+ await updateUser({ password: newPassword })
254
+ showSuccess('Password updated.')
255
+ } catch (error) {
256
+ if (error instanceof AuthError) showError(error.message)
257
+ }
258
+ }
259
+ ```
260
+
261
+ Note: The recovery callback fires a `'recovery'` auth event, not `'login'`. The user is authenticated but should be
262
+ prompted to set a new password before navigating away.
263
+
264
+ ### Invite Acceptance
265
+
266
+ When a user clicks an invite link, `handleAuthCallback()` returns `{ type: 'invite', user: null, token }`. Use the
267
+ token to accept the invite and set a password.
268
+
269
+ ```typescript
270
+ import { acceptInvite, AuthError } from '@netlify/identity'
271
+
272
+ async function handleAcceptInvite(token: string, password: string) {
273
+ try {
274
+ const user = await acceptInvite(token, password)
275
+ showSuccess(`Welcome, ${user.email}! Your account is ready.`)
276
+ } catch (error) {
277
+ if (error instanceof AuthError) showError(error.message)
278
+ }
279
+ }
280
+ ```
281
+
282
+ ### Email Change
283
+
284
+ When a user verifies an email change, `handleAuthCallback()` returns `{ type: 'email_change', user }`. This requires an
285
+ active browser session - the user must be logged in when clicking the verification link.
286
+
287
+ ```typescript
288
+ import { verifyEmailChange, AuthError } from '@netlify/identity'
289
+
290
+ // If you need to verify programmatically with a token:
291
+ async function handleEmailChangeVerification(token: string) {
292
+ try {
293
+ const user = await verifyEmailChange(token)
294
+ showSuccess(`Email updated to ${user.email}`)
295
+ } catch (error) {
296
+ if (error instanceof AuthError) showError(error.message)
297
+ }
298
+ }
299
+ ```
300
+
301
+ ### Session Hydration
302
+
303
+ `hydrateSession()` bridges server-set cookies to the browser session. Call it on page load when using server-side login
304
+ (e.g., login inside a Netlify Function followed by a redirect).
305
+
306
+ ```typescript
307
+ import { hydrateSession } from '@netlify/identity'
308
+
309
+ // On page load
310
+ const user = await hydrateSession()
311
+ if (user) {
312
+ // Browser session is now in sync with server-set cookies
313
+ }
314
+ ```
315
+
316
+ Note: `getUser()` auto-hydrates from the `nf_jwt` cookie if no browser session exists, so explicit `hydrateSession()`
317
+ is only needed when you want to restore the full session (including token refresh timers) after a server-side login.
318
+
319
+ ## Auth Events
320
+
321
+ Subscribe to auth state changes with `onAuthChange`. Returns an unsubscribe function. No-op on server.
322
+
323
+ ```typescript
324
+ import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
325
+
326
+ const unsubscribe = onAuthChange((event, user) => {
327
+ switch (event) {
328
+ case AUTH_EVENTS.LOGIN:
329
+ console.log('User logged in:', user?.email)
330
+ break
331
+ case AUTH_EVENTS.LOGOUT:
332
+ console.log('User logged out')
333
+ break
334
+ case AUTH_EVENTS.TOKEN_REFRESH:
335
+ // Token auto-refreshed in background
336
+ break
337
+ case AUTH_EVENTS.USER_UPDATED:
338
+ console.log('User profile updated:', user?.email)
339
+ break
340
+ case AUTH_EVENTS.RECOVERY:
341
+ // Recovery token processed - prompt for new password
342
+ console.log('Password recovery initiated')
343
+ break
344
+ }
345
+ })
346
+
347
+ // Later: unsubscribe()
348
+ ```
349
+
350
+ Auth events are automatically detected across browser tabs via the storage event listener - no extra setup needed.
351
+
352
+ ## Settings-Driven UI
353
+
354
+ Fetch the project's Identity settings to conditionally render signup forms and OAuth buttons.
355
+
356
+ ```typescript
357
+ import { getSettings } from '@netlify/identity'
358
+
359
+ const settings = await getSettings()
360
+ // settings.autoconfirm - boolean, whether email confirmation is skipped
361
+ // settings.disableSignup - boolean, whether registration is closed
362
+ // settings.providers - Record<AuthProvider, boolean>, e.g. { google: true, github: true, ... }
363
+
364
+ // Conditionally render signup
365
+ if (!settings.disableSignup) {
366
+ showSignupForm()
367
+ }
368
+
369
+ // Conditionally render OAuth buttons
370
+ for (const [provider, enabled] of Object.entries(settings.providers)) {
371
+ if (enabled) {
372
+ showOAuthButton(provider)
373
+ }
374
+ }
375
+ ```
376
+
377
+ ## Full API Reference
378
+
379
+ For the complete API reference - all function signatures, type definitions, OAuth helpers, admin operations, session
380
+ management, auth events, and framework-specific integration examples - read the package README:
381
+
382
+ ```
383
+ node_modules/@netlify/identity/README.md
384
+ ```
385
+
386
+ The README is shipped with the npm package and is always in sync with the installed version.
387
+
388
+ ## SSR Integration Patterns
389
+
390
+ For SSR frameworks, the recommended pattern is:
391
+
392
+ - **Browser-side** for auth mutations: `login()`, `signup()`, `logout()`, `oauthLogin()`
393
+ - **Server-side** for reading auth state: `getUser()`, `getSettings()`, `getIdentityConfig()`
394
+
395
+ Browser-side auth mutations set the `nf_jwt` cookie and localStorage, and emit `onAuthChange` events. The
396
+ server reads the cookie on the next request.
397
+
398
+ The library also supports server-side mutations (`login()`, `signup()`, `logout()` inside Netlify Functions), but these
399
+ require the Netlify Functions runtime to set cookies. After a server-side mutation, use a full page navigation so the
400
+ browser sends the new cookie.
401
+
402
+ Always use `window.location.href` (not framework router navigation) after server-side auth mutations in Next.js,
403
+ TanStack Start, and SvelteKit. Remix `redirect()` is safe because Remix actions return real HTTP responses.
404
+
405
+ ## Identity Event Functions
406
+
407
+ Special serverless functions that trigger on Identity lifecycle events. These use the **legacy named `handler` export**
408
+ (not the modern default export) because they receive `event.body` containing the user payload.
409
+
410
+ Always use the legacy named `handler` export (not default export) for Identity event functions. The filename must match
411
+ the event name exactly (e.g., `netlify/functions/identity-signup.mts`).
412
+
413
+ **Event names:** `identity-validate`, `identity-signup`, `identity-login`
414
+
415
+ - `identity-signup` - fires when a new user signs up (email/password or OAuth)
416
+ - `identity-login` - fires on each login
417
+ - `identity-validate` - fires during signup before the user is created; return a non-200 status to reject
418
+
419
+ ### Example: Assign Default Role on Signup
420
+
421
+ ```typescript
422
+ // netlify/functions/identity-signup.mts
423
+ import type { Handler, HandlerEvent, HandlerContext } from '@netlify/functions'
424
+
425
+ const handler: Handler = async (event: HandlerEvent, context: HandlerContext) => {
426
+ const { user } = JSON.parse(event.body || '{}')
427
+
428
+ return {
429
+ statusCode: 200,
430
+ body: JSON.stringify({
431
+ app_metadata: {
432
+ ...user.app_metadata,
433
+ roles: ['member'],
434
+ },
435
+ }),
436
+ }
437
+ }
438
+
439
+ export { handler }
440
+ ```
441
+
442
+ The response body replaces `app_metadata` and/or `user_metadata` on the user record - include all fields you want to
443
+ keep, not just new ones.
444
+
445
+ For bulk user management or role changes outside lifecycle events, use the `admin` API instead of Identity event
446
+ functions.
447
+
448
+ ## Roles and Authorization
449
+
450
+ ### First Admin User
451
+
452
+ The first admin user cannot be created through code alone. You must direct the user to set it up through the Netlify UI:
453
+
454
+ 1. Go to **Identity** in the project sidebar in the Netlify dashboard
455
+ 2. Click **Invite users** and enter the admin user's email address
456
+ 3. After the user accepts the invite, click the user in the Identity list to open their detail page
457
+ 4. In the **Roles** field, add the `admin` role and save
458
+
459
+ Once the first admin exists, subsequent users can be managed programmatically using Identity event functions (e.g., assigning roles in `identity-signup`) or role-based redirects.
460
+
461
+ ### Metadata Types
462
+
463
+ - **`app_metadata.roles`** - Server-controlled. Only settable via the Netlify UI, admin API, or Identity event functions.
464
+ Do not allow users to set their own roles.
465
+ - **`user_metadata`** - User-controlled. Users can update this via `updateUser({ data: { ... } })`.
466
+
467
+ ### Role-Based Redirects
468
+
469
+ Use `netlify.toml` to restrict paths by role:
470
+
471
+ ```toml
472
+ [[redirects]]
473
+ from = "/admin/*"
474
+ to = "/admin/:splat"
475
+ status = 200
476
+ conditions = { Role = ["admin"] }
477
+
478
+ [[redirects]]
479
+ from = "/admin/*"
480
+ to = "/"
481
+ status = 302
482
+ ```
483
+
484
+ Rules are evaluated top-to-bottom. The first redirect matches users with the `admin` role; everyone else falls through
485
+ to the second rule and is redirected away.
486
+
487
+ **How it works:** The `nf_jwt` cookie is read by the CDN to evaluate `conditions = { Role = [...] }`. Without this
488
+ cookie, role-based redirects will not work.
489
+
490
+ ## Common Errors & Solutions
491
+
492
+ ### "Signups not allowed for this instance" (403)
493
+
494
+ **Cause:** Registration is set to Invite only.
495
+
496
+ **Fix:**
497
+
498
+ 1. Change to Open in **Project configuration > Identity**
499
+ 2. Or invite users from the Identity tab in the Netlify UI
500
+
501
+ ```typescript
502
+ if (error instanceof AuthError && error.status === 403) {
503
+ showError('Signups are disabled. Contact the site admin for an invite.')
504
+ }
505
+ ```
506
+
507
+ ### Invalid credentials (401)
508
+
509
+ **Cause:** Wrong email or password on login.
510
+
511
+ **Fix:** Show a user-facing error and let the user retry. Do not reveal whether the email or password was wrong.
512
+
513
+ ```typescript
514
+ if (error instanceof AuthError && error.status === 401) {
515
+ showError('Invalid email or password.')
516
+ }
517
+ ```
518
+
519
+ ### "Email not confirmed"
520
+
521
+ **Cause:** User tries to log in before confirming their email (autoconfirm is off).
522
+
523
+ **Fix:** Tell the user to check their inbox. Optionally provide a way to resend the confirmation email via `signup()`
524
+ with the same credentials.
525
+
526
+ ### "Token expired" / 401 on API calls
527
+
528
+ **Cause:** Stale access token.
529
+
530
+ **Fix:**
531
+
532
+ 1. Always use `getUser()` before authenticated requests - it auto-hydrates from cookies
533
+ 2. In the browser, the library auto-refreshes tokens via `startTokenRefresh()`
534
+ 3. On the server, call `refreshSession()` in middleware to handle near-expiry tokens
535
+
536
+ ```typescript
537
+ const newToken = await refreshSession()
538
+ if (newToken) {
539
+ // Token was refreshed - retry the request
540
+ }
541
+ ```
542
+
543
+ ### Identity event function not triggering
544
+
545
+ **Cause:** Filename or export format does not match expected convention.
546
+
547
+ **Fix:**
548
+
549
+ 1. Verify filename matches exactly: `identity-signup`, `identity-validate`, or `identity-login`
550
+ 2. Place in `netlify/functions/` with `.mts` or `.mjs` extension
551
+ 3. Use named `handler` export (not default export)
552
+
553
+ ### `MissingIdentityError`
554
+
555
+ **Cause:** Identity is not configured in the current environment.
556
+
557
+ **Fix:**
558
+
559
+ 1. Ensure Identity is enabled on the project
560
+ 2. Use `netlify dev` for local development so the Identity endpoint is available
561
+
562
+ ```typescript
563
+ if (error instanceof MissingIdentityError) {
564
+ showError('Identity is not enabled. Run "netlify dev" or enable Identity in project settings.')
565
+ }
566
+ ```
567
+
568
+ ### `AuthError` on server - missing Netlify runtime
569
+
570
+ **Cause:** Server-side `login()`, `signup()`, or `logout()` require the Netlify Functions runtime to set cookies.
571
+
572
+ **Fix:**
573
+
574
+ 1. Deploy to Netlify to use server-side auth mutations
575
+ 2. Or use `netlify dev` for local development
576
+
577
+ ### "User not found" after OAuth login
578
+
579
+ **Cause:** OAuth provider is not enabled for the project.
580
+
581
+ **Fix:**
582
+
583
+ 1. Enable the provider in **Project configuration > Identity > External providers**
584
+ 2. Users are created automatically on first OAuth login
585
+
586
+ ### Account operations fail after server-side login
587
+
588
+ **Cause:** Browser-side session is not bootstrapped from server-set cookies.
589
+
590
+ **Fix:** Call `hydrateSession()` on page load to bridge server-set cookies to the browser session. Then use
591
+ `updateUser()`, `verifyEmailChange()`, or other account operations.
592
+
593
+ ```typescript
594
+ import { hydrateSession, updateUser } from '@netlify/identity'
595
+
596
+ // On page load after server-side login
597
+ await hydrateSession()
598
+
599
+ // Now account operations work
600
+ await updateUser({ data: { full_name: 'Jane' } })
601
+ ```
602
+
603
+ ### "Email change verification requires an active browser session"
604
+
605
+ **Cause:** `verifyEmailChange()` was called without an active session. The user must be logged in when clicking the
606
+ email change verification link.
607
+
608
+ **Fix:** Ensure the user is logged in before processing the `email_change` callback. If the session expired, prompt
609
+ the user to log in first.
610
+
611
+ ### "No user is currently logged in"
612
+
613
+ **Cause:** An account operation (`updateUser`, `verifyEmailChange`) was called without an authenticated user.
614
+
615
+ **Fix:** Check `getUser()` before calling account operations. If `null`, redirect to login.
616
+
617
+ ```typescript
618
+ const user = await getUser()
619
+ if (!user) {
620
+ redirectToLogin()
621
+ return
622
+ }
623
+ await updateUser({ data: { full_name: 'Jane' } })
624
+ ```
625
+
626
+ ### Stale session - user deleted server-side
627
+
628
+ **Cause:** `getUser()` returns `null` when the `nf_jwt` cookie is gone but localStorage still has a stale
629
+ session. This happens when a user is deleted via the admin API or Netlify UI.
630
+
631
+ **Fix:** `getUser()` handles this gracefully - it returns `null` and the stale localStorage entry is ignored. Always
632
+ check for `null` before using the user object.
@@ -0,0 +1,12 @@
1
+ const { execFileSync } = require('child_process')
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+
5
+ const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf-8' }).trim()
6
+ const featuresDir = path.join(repoRoot, '.netlify', 'features')
7
+ const markerFile = path.join(featuresDir, 'netlify-identity')
8
+
9
+ fs.mkdirSync(featuresDir, { recursive: true })
10
+ fs.writeFileSync(markerFile, '')
11
+
12
+ console.log('Netlify Identity feature enabled for this site.')