@jskit-ai/agent-docs 0.1.130 → 0.1.132
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.md +29 -605
- package/guide/agent/app-extras/mobile-capacitor.md +29 -362
- package/guide/agent/app-extras/realtime.md +29 -277
- package/guide/agent/app-setup/a-more-interesting-shell.md +44 -815
- package/guide/agent/app-setup/authentication.md +43 -1073
- package/guide/agent/app-setup/console.md +26 -298
- package/guide/agent/app-setup/database-layer.md +110 -790
- package/guide/agent/app-setup/initial-scaffolding.md +50 -784
- package/guide/agent/app-setup/multi-homing.md +39 -712
- package/guide/agent/app-setup/quickstart.md +43 -179
- package/guide/agent/app-setup/users.md +34 -353
- package/guide/agent/index.md +16 -23
- package/package.json +2 -2
- package/patterns/INDEX.md +7 -7
- package/patterns/child-cruds.md +3 -3
- package/patterns/client-requests.md +6 -6
- package/patterns/crud-authoring.md +94 -0
- package/patterns/crud-links.md +1 -1
- package/patterns/feature-package/PATTERN.md +108 -0
- package/patterns/feature-package/example/booking-engine/package.json +48 -0
- package/patterns/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +33 -0
- package/patterns/feature-package/example/booking-engine/src/server/actions.js +26 -0
- package/patterns/feature-package/example/booking-engine/src/server/inputSchemas.js +19 -0
- package/patterns/feature-package/example/variations/AvailabilityEngineProvider.js +31 -0
- package/patterns/feature-package/example/variations/InvoiceRollupProvider.js +36 -0
- package/patterns/feature-package/example/variations/customKnexRepository.js +34 -0
- package/patterns/feature-package/example/variations/orchestratorService.js +23 -0
- package/patterns/filters.md +8 -8
- package/patterns/live-actions.md +5 -18
- package/patterns/minimal-foundation/PATTERN.md +98 -0
- package/patterns/minimal-foundation/example/.nvmrc +1 -0
- package/patterns/minimal-foundation/example/AGENTS.md +17 -0
- package/patterns/minimal-foundation/example/Procfile +2 -0
- package/patterns/minimal-foundation/example/app.json +14 -0
- package/patterns/minimal-foundation/example/bin/develop.js +71 -0
- package/patterns/minimal-foundation/example/bin/server.js +8 -0
- package/patterns/minimal-foundation/example/config/public.js +40 -0
- package/patterns/minimal-foundation/example/config/server.js +1 -0
- package/patterns/minimal-foundation/example/config/surfaceAccessPolicies.js +3 -0
- package/patterns/minimal-foundation/example/eslint.config.mjs +19 -0
- package/patterns/minimal-foundation/example/favicon.svg +7 -0
- package/patterns/minimal-foundation/example/gitignore +9 -0
- package/patterns/minimal-foundation/example/index.html +13 -0
- package/patterns/minimal-foundation/example/jsconfig.json +8 -0
- package/patterns/minimal-foundation/example/package.json +57 -0
- package/patterns/minimal-foundation/example/packages/main/package.json +42 -0
- package/patterns/minimal-foundation/example/packages/main/src/shared/index.js +10 -0
- package/patterns/minimal-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
- package/patterns/minimal-foundation/example/playwright.config.mjs +31 -0
- package/patterns/minimal-foundation/example/server/lib/runtimeEnv.js +45 -0
- package/patterns/minimal-foundation/example/server/lib/surfaceRuntime.js +10 -0
- package/patterns/minimal-foundation/example/server.js +195 -0
- package/patterns/minimal-foundation/example/src/App.vue +13 -0
- package/patterns/minimal-foundation/example/src/main.js +85 -0
- package/patterns/minimal-foundation/example/src/pages/home/index.vue +48 -0
- package/patterns/minimal-foundation/example/src/pages/home.vue +13 -0
- package/patterns/minimal-foundation/example/src/views/NotFound.vue +13 -0
- package/patterns/minimal-foundation/example/tests/client/smoke.vitest.js +7 -0
- package/patterns/minimal-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
- package/patterns/minimal-foundation/example/tests/server/smoke.test.js +16 -0
- package/patterns/minimal-foundation/example/vite.config.mjs +81 -0
- package/patterns/page-scaffolding.md +20 -17
- package/patterns/placements.md +17 -15
- package/patterns/row-policies.md +4 -5
- package/patterns/server-search.md +3 -3
- package/patterns/shell-foundation/PATTERN.md +104 -0
- package/patterns/shell-foundation/example/.nvmrc +1 -0
- package/patterns/shell-foundation/example/AGENTS.md +17 -0
- package/patterns/shell-foundation/example/Procfile +2 -0
- package/patterns/shell-foundation/example/app.json +14 -0
- package/patterns/shell-foundation/example/bin/develop.js +71 -0
- package/patterns/shell-foundation/example/bin/server.js +8 -0
- package/patterns/shell-foundation/example/config/public.js +40 -0
- package/patterns/shell-foundation/example/config/server.js +1 -0
- package/patterns/shell-foundation/example/config/surfaceAccessPolicies.js +3 -0
- package/patterns/shell-foundation/example/eslint.config.mjs +19 -0
- package/patterns/shell-foundation/example/favicon.svg +7 -0
- package/patterns/shell-foundation/example/gitignore +9 -0
- package/patterns/shell-foundation/example/index.html +13 -0
- package/patterns/shell-foundation/example/jsconfig.json +8 -0
- package/patterns/shell-foundation/example/package.json +59 -0
- package/patterns/shell-foundation/example/packages/main/package.json +56 -0
- package/patterns/shell-foundation/example/packages/main/src/client/index.js +9 -0
- package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +18 -0
- package/patterns/shell-foundation/example/packages/main/src/shared/index.js +10 -0
- package/patterns/shell-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
- package/patterns/shell-foundation/example/playwright.config.mjs +31 -0
- package/patterns/shell-foundation/example/server/lib/runtimeEnv.js +45 -0
- package/patterns/shell-foundation/example/server/lib/surfaceRuntime.js +10 -0
- package/patterns/shell-foundation/example/server.js +195 -0
- package/patterns/shell-foundation/example/src/App.vue +11 -0
- package/patterns/shell-foundation/example/src/components/ShellLayout.vue +12 -0
- package/patterns/shell-foundation/example/src/components/menus/MenuLinkItem.vue +30 -0
- package/patterns/shell-foundation/example/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
- package/patterns/shell-foundation/example/src/components/menus/TabLinkItem.vue +42 -0
- package/patterns/shell-foundation/example/src/error.js +19 -0
- package/patterns/shell-foundation/example/src/main.js +85 -0
- package/patterns/shell-foundation/example/src/pages/home/index.vue +116 -0
- package/patterns/shell-foundation/example/src/pages/home/settings/general/index.vue +40 -0
- package/patterns/shell-foundation/example/src/pages/home/settings/index.vue +7 -0
- package/patterns/shell-foundation/example/src/pages/home/settings.vue +109 -0
- package/patterns/shell-foundation/example/src/pages/home.vue +20 -0
- package/patterns/shell-foundation/example/src/placement.js +56 -0
- package/patterns/shell-foundation/example/src/placementTopology.js +149 -0
- package/patterns/shell-foundation/example/src/views/NotFound.vue +13 -0
- package/patterns/shell-foundation/example/tests/client/smoke.vitest.js +7 -0
- package/patterns/shell-foundation/example/tests/e2e/adaptive-shell.spec.ts +10 -0
- package/patterns/shell-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
- package/patterns/shell-foundation/example/tests/server/smoke.test.js +16 -0
- package/patterns/shell-foundation/example/vite.config.mjs +81 -0
- package/patterns/ui-contract.md +56 -0
- package/patterns/ui-testing.md +10 -12
- package/reference/autogen/KERNEL_MAP.md +29 -107
- package/reference/autogen/PATTERN_INDEX.md +230 -0
- package/reference/autogen/README.md +4 -8
- package/reference/autogen/packages/agent-docs.md +259 -0
- package/reference/autogen/packages/assistant-core.md +3 -3
- package/reference/autogen/packages/assistant-runtime.md +32 -17
- package/reference/autogen/packages/auth-core.md +31 -33
- package/reference/autogen/packages/auth-provider-local-core.md +4 -12
- package/reference/autogen/packages/auth-provider-local-db-core.md +4 -4
- package/reference/autogen/packages/auth-provider-supabase-core.md +14 -18
- package/reference/autogen/packages/auth-web.md +42 -22
- package/reference/autogen/packages/console-core.md +8 -25
- package/reference/autogen/packages/console-web.md +5 -5
- package/reference/autogen/packages/crud-core.md +61 -17
- package/reference/autogen/packages/database-runtime-mysql.md +12 -2
- package/reference/autogen/packages/database-runtime-postgres.md +12 -2
- package/reference/autogen/packages/database-runtime.md +26 -25
- package/reference/autogen/packages/google-rewarded-core.md +19 -104
- package/reference/autogen/packages/http-runtime.md +4 -8
- package/reference/autogen/packages/http-web.md +32 -0
- package/reference/autogen/packages/json-rest-api-core.md +4 -6
- package/reference/autogen/packages/kernel.md +109 -390
- package/reference/autogen/packages/mobile-capacitor.md +2 -13
- package/reference/autogen/packages/realtime.md +29 -26
- package/reference/autogen/packages/resource-crud-core.md +6 -0
- package/reference/autogen/packages/shell-web.md +69 -54
- package/reference/autogen/packages/storage-runtime.md +3 -3
- package/reference/autogen/packages/uploads-image-web.md +0 -1
- package/reference/autogen/packages/uploads-runtime.md +3 -3
- package/reference/autogen/packages/users-core.md +45 -90
- package/reference/autogen/packages/users-web.md +5 -7
- package/reference/autogen/packages/workspaces-core.md +53 -74
- package/reference/autogen/packages/workspaces-web.md +15 -16
- package/reference/autogen/tooling/jskit-catalog.md +34 -0
- package/reference/autogen/tooling/testUtils.md +4 -4
- package/skills/jskit/SKILL.md +36 -28
- package/skills/jskit/agents/openai.yaml +2 -2
- package/skills/jskit/references/app-operations.md +68 -53
- package/skills/jskit/references/crud-operations.md +58 -106
- package/skills/jskit/references/material-3.md +105 -0
- package/skills/jskit/references/ui-operations.md +41 -44
- package/templates/app/AGENTS.md +7 -3
- package/guide/agent/app-setup/upgrade-beta-1-to-final.md +0 -252
- package/guide/agent/app-setup/working-with-the-jskit-cli.md +0 -325
- package/guide/agent/generators/advanced-cruds.md +0 -1935
- package/guide/agent/generators/crud-generators.md +0 -948
- package/guide/agent/generators/intro.md +0 -65
- package/guide/agent/generators/row-policies.md +0 -537
- package/guide/agent/generators/ui-generators.md +0 -690
- package/patterns/crud-scaffolding.md +0 -198
- package/patterns/generated-ui-contract-tracking.md +0 -66
- package/reference/autogen/packages/assistant.md +0 -68
- package/reference/autogen/packages/crud-server-generator.md +0 -215
- package/reference/autogen/packages/crud-ui-generator.md +0 -192
- package/reference/autogen/packages/feature-server-generator.md +0 -65
- package/reference/autogen/packages/ui-generator.md +0 -127
- package/reference/autogen/tooling/create-app.md +0 -317
- package/reference/autogen/tooling/jskit-cli.md +0 -933
- package/reference/autogen/tooling/test-support.md +0 -27
|
@@ -2,1098 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
# Authentication
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
JSKIT separates provider-neutral authentication behavior, one selected identity
|
|
6
|
+
provider, and the browser authentication surface.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
To get back to the same starting point as the end of the previous chapter, run:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
npx @jskit-ai/create-app exampleapp --tenancy-mode none
|
|
13
|
-
cd exampleapp
|
|
14
|
-
npm install
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
If you are already continuing from the previous chapter, you are already in the right place and can skip that setup.
|
|
18
|
-
|
|
19
|
-
**Default: Start With Local Auth**
|
|
20
|
-
|
|
21
|
-
The default auth path does not require a Supabase project or a database. It uses the local provider package, `auth-provider-local-core`, together with `auth-web`. The local provider stores credentials and sessions in `.jskit/auth/` by default.
|
|
22
|
-
|
|
23
|
-
That is the recommended first step for most apps. After the app's core flow is working, switch to Supabase or add richer auth methods only if the product needs them.
|
|
24
|
-
|
|
25
|
-
## Installing the auth layer
|
|
26
|
-
|
|
27
|
-
From inside `exampleapp`, run:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
npx jskit add package auth-provider-local-core
|
|
31
|
-
npx jskit add package auth-web
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
These package installs add the provider-neutral auth core, the auth web layer, and the local provider. They give the app working register, login, logout, session, and password recovery flows without requiring an external auth service.
|
|
35
|
-
|
|
36
|
-
The final `npm install` matters for the same reason it did in the shell chapter: `jskit add` rewrites the scaffold and updates `package.json`, but `npm install` is what actually downloads the newly referenced runtime packages.
|
|
37
|
-
|
|
38
|
-
## Running it
|
|
39
|
-
|
|
40
|
-
Authentication needs both the browser-facing dev server and the backend runtime, so start both:
|
|
8
|
+
For local authentication:
|
|
41
9
|
|
|
42
10
|
```bash
|
|
43
|
-
npm
|
|
44
|
-
npm run server
|
|
11
|
+
npm install @jskit-ai/auth-provider-local-core @jskit-ai/auth-web
|
|
45
12
|
```
|
|
46
13
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
The login page is real and renders only the modes supported by the active provider.
|
|
50
|
-
|
|
51
|
-
- **Sign in** is the normal email-and-password flow.
|
|
52
|
-
- **Register** creates a local auth user.
|
|
53
|
-
- **Forgot password?** requests a password reset link when recovery is configured.
|
|
54
|
-
- **Remember this account on this device** stores a small local hint in browser storage so the next visit can greet the last-used account and let the user keep that email preselected.
|
|
55
|
-
|
|
56
|
-
If you go back to `http://localhost:5173/home`, the shell also has a small auth widget in the status area. When you are signed out it shows a guest state and a menu entry that leads to `/auth/login`. After you sign in, the same placement changes to a sign-out menu.
|
|
57
|
-
|
|
58
|
-
## Reading the screen carefully
|
|
59
|
-
|
|
60
|
-
The login card is not a single form. It is a small state machine that switches between several modes.
|
|
61
|
-
|
|
62
|
-
### Normal sign-in mode
|
|
63
|
-
|
|
64
|
-
This is the first screen you see.
|
|
65
|
-
|
|
66
|
-
- The `Email` field is validated as an email address.
|
|
67
|
-
- The `Password` field is validated with the password rules from the shared auth command schema.
|
|
68
|
-
- The eye icon only changes whether the password is visible in the browser.
|
|
69
|
-
- The main `Sign in` button posts to `POST /api/login`.
|
|
70
|
-
- If sign-in succeeds, JSKIT refreshes the current session and redirects back to the requested return path.
|
|
71
|
-
|
|
72
|
-
The small links under the password field are not decoration.
|
|
73
|
-
|
|
74
|
-
- `Forgot password?` switches the card into password-reset-request mode.
|
|
75
|
-
- `Use one-time code` switches the card into OTP mode when the active provider supports OTP. The local provider does not show this link.
|
|
76
|
-
|
|
77
|
-
### Register mode
|
|
78
|
-
|
|
79
|
-
When the user presses `Register`, the form changes in three important ways.
|
|
80
|
-
|
|
81
|
-
- The screen shows the `Create your account` title.
|
|
82
|
-
- A `Confirm password` field appears.
|
|
83
|
-
- The main submit button changes from `Sign in` to `Register`.
|
|
84
|
-
|
|
85
|
-
Pressing `Register` sends a sign-up request through `POST /api/register`. With the local provider, JSKIT writes the local auth record and creates a session immediately.
|
|
86
|
-
|
|
87
|
-
What happens next depends on the active provider's capabilities.
|
|
88
|
-
|
|
89
|
-
- If email confirmation is required, the provider creates the user but does not return a live session. The card then switches to a dedicated confirmation state.
|
|
90
|
-
- If email confirmation is not required, the user is registered and signed in immediately.
|
|
91
|
-
|
|
92
|
-
### Email-confirmation state
|
|
93
|
-
|
|
94
|
-
If the active provider requires confirmation, the screen changes again. The local provider does not require email confirmation, so most first apps will not see this mode.
|
|
95
|
-
|
|
96
|
-
- The mode-switch buttons disappear.
|
|
97
|
-
- The card shows a confirmation message instead of the email/password fields.
|
|
98
|
-
- `Go to main screen` returns to the app.
|
|
99
|
-
- `Resend confirmation email` calls `POST /api/register/confirmation/resend`.
|
|
100
|
-
- `Back to sign in` returns to the normal login mode.
|
|
101
|
-
|
|
102
|
-
This matters because a junior developer might otherwise assume registration failed. In reality, the provider may already have created the auth identity; the browser just does not have an active session yet.
|
|
103
|
-
|
|
104
|
-
### One-time-code mode
|
|
105
|
-
|
|
106
|
-
When a provider supports OTP, `Use one-time code` changes the form again.
|
|
107
|
-
|
|
108
|
-
- The password field disappears.
|
|
109
|
-
- A `One-time code` field appears.
|
|
110
|
-
- A secondary button called `Send one-time code` appears.
|
|
111
|
-
- The main button changes to `Verify code`.
|
|
112
|
-
|
|
113
|
-
Those two buttons do different jobs.
|
|
114
|
-
|
|
115
|
-
- `Send one-time code` requests the email through `POST /api/login/otp/request`.
|
|
116
|
-
- `Verify code` submits the code through `POST /api/login/otp/verify`.
|
|
117
|
-
|
|
118
|
-
The local provider does not implement OTP. Supabase and future providers can expose this mode through the same capability contract. In other words, OTP is an opt-in sign-in method, not something the first app needs up front.
|
|
119
|
-
|
|
120
|
-
### Password-reset-request mode
|
|
121
|
-
|
|
122
|
-
`Forgot password?` does not immediately let the user type a new password. It switches to a reset-request mode.
|
|
123
|
-
|
|
124
|
-
- The password field disappears.
|
|
125
|
-
- The main button changes to `Send reset instructions`.
|
|
126
|
-
- Submitting this screen calls `POST /api/password/forgot`.
|
|
127
|
-
|
|
128
|
-
That endpoint asks the active provider to start password recovery. With the local provider, SMTP is used when configured; otherwise local development can log or return the recovery URL depending on environment settings.
|
|
129
|
-
|
|
130
|
-
The recovery link opens `/auth/reset-password`. That page exchanges the recovery token for a short-lived recovery-scoped session, lets the user choose a new password, and then clears the recovery session.
|
|
131
|
-
|
|
132
|
-
### Remembered account behavior
|
|
133
|
-
|
|
134
|
-
The checkbox `Remember this account on this device` is also worth understanding.
|
|
135
|
-
|
|
136
|
-
If it stays checked and the user successfully signs in, registers, or verifies an OTP code, JSKIT stores a small browser-local hint with:
|
|
137
|
-
|
|
138
|
-
- the normalized email address
|
|
139
|
-
- a masked version of the email
|
|
140
|
-
- a display name
|
|
141
|
-
- a `lastUsedAt` timestamp
|
|
142
|
-
|
|
143
|
-
On the next visit, the card can show a `Welcome back, ...` panel and a `Use another account` button. This is only a browser convenience feature. It is not a second authentication factor, and it is not a server-side session store.
|
|
144
|
-
|
|
145
|
-
### OAuth buttons
|
|
146
|
-
|
|
147
|
-
The screen is also ready to show OAuth provider buttons, but only if the active provider supports OAuth and providers are configured. Local auth intentionally keeps this off.
|
|
148
|
-
|
|
149
|
-
For a later Supabase setup, `config.server.js` can keep this empty at first:
|
|
150
|
-
|
|
151
|
-
```js
|
|
152
|
-
config.auth ||= {};
|
|
153
|
-
config.auth.profileMode = "provider";
|
|
154
|
-
config.auth.oauth = {
|
|
155
|
-
providers: [],
|
|
156
|
-
defaultProvider: ""
|
|
157
|
-
};
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
That is why the login card does not show buttons like `Continue with Google`.
|
|
161
|
-
|
|
162
|
-
After switching to a provider that supports OAuth, turning on Google has two separate setup steps.
|
|
163
|
-
|
|
164
|
-
First, configure Google and Supabase:
|
|
165
|
-
|
|
166
|
-
1. In Google Auth Platform, create a **Web application** OAuth client.
|
|
167
|
-
2. Add your browser URLs as **Authorized JavaScript origins**.
|
|
168
|
-
3. In Supabase, open the Google provider settings and copy the provider callback URL shown there.
|
|
169
|
-
4. Add that Supabase callback URL as an **Authorized redirect URI** on the Google OAuth client.
|
|
170
|
-
5. Back in Supabase, paste the Google **Client ID** and **Client Secret** into the Google provider settings and enable the provider.
|
|
171
|
-
6. Make sure Supabase's **Site URL** and **Redirect URLs** still match the real browser URL your app uses.
|
|
172
|
-
|
|
173
|
-
Then tell JSKIT to expose the provider in the login UI:
|
|
174
|
-
|
|
175
|
-
```js
|
|
176
|
-
config.auth ||= {};
|
|
177
|
-
config.auth.oauth = {
|
|
178
|
-
providers: ["google"],
|
|
179
|
-
defaultProvider: "google"
|
|
180
|
-
};
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
`providers` controls which OAuth buttons the stock login screen is allowed to render. `defaultProvider` tells JSKIT which provider to prefer when it needs a default choice. If the provider is configured in Supabase but missing from this list, the button still does not appear in the JSKIT login screen.
|
|
184
|
-
|
|
185
|
-
**Important: What Works Without A Database**
|
|
186
|
-
|
|
187
|
-
Authentication is already real in this chapter because the local provider owns the auth data it needs:
|
|
188
|
-
|
|
189
|
-
- local auth user records
|
|
190
|
-
- password hashes
|
|
191
|
-
- recovery tokens
|
|
192
|
-
- access and refresh sessions
|
|
193
|
-
- the HTTP-only cookies JSKIT writes for the browser
|
|
194
|
-
|
|
195
|
-
What is missing is JSKIT's own database-backed users layer. That is intentional for the first app. Login should not require MySQL, Postgres, Supabase, or a generated users table before the product itself exists.
|
|
196
|
-
|
|
197
|
-
Concretely, that means:
|
|
198
|
-
|
|
199
|
-
- JSKIT can register a user.
|
|
200
|
-
- JSKIT can sign that user in and out.
|
|
201
|
-
- JSKIT can read the current session.
|
|
202
|
-
- JSKIT can request and complete password recovery.
|
|
203
|
-
- JSKIT can run without any JSKIT database runtime package.
|
|
204
|
-
|
|
205
|
-
By default, the local provider stores its runtime state under `.jskit/auth/`:
|
|
206
|
-
|
|
207
|
-
- `users.passwd`
|
|
208
|
-
- `sessions.passwd`
|
|
209
|
-
- `recovery.passwd`
|
|
210
|
-
- `session.secret`
|
|
211
|
-
|
|
212
|
-
Those files are local runtime state, not generated application code. They are ignored by git.
|
|
213
|
-
|
|
214
|
-
So without a database, you still get:
|
|
215
|
-
|
|
216
|
-
- real login
|
|
217
|
-
- real logout
|
|
218
|
-
- real registration
|
|
219
|
-
- real password reset requests and completions
|
|
220
|
-
- real session cookies
|
|
221
|
-
|
|
222
|
-
But you do **not** get:
|
|
223
|
-
|
|
224
|
-
- persistent JSKIT-side user rows
|
|
225
|
-
- persistent JSKIT-side account settings
|
|
226
|
-
- OAuth, OTP, or provider linking from the local provider
|
|
227
|
-
- workspace membership, user preferences, or the later users/workspaces data model
|
|
228
|
-
|
|
229
|
-
Later, when an app needs the database-backed users layer, `users-core` can provide `auth.profile.projector` through `users.profile.sync.service`.
|
|
230
|
-
|
|
231
|
-
That service projects a provider identity into the app-owned user/profile model. Local auth, Supabase auth, and future auth providers can all use that same projection seam.
|
|
232
|
-
|
|
233
|
-
So this chapter gives you real authentication, but it deliberately keeps the user model small. The full persistent JSKIT user model comes later with the database and users layers.
|
|
234
|
-
|
|
235
|
-
## Using auth in your own app
|
|
236
|
-
|
|
237
|
-
The most important thing this chapter gives you is not just a login page. It gives you three real app-building tools:
|
|
238
|
-
|
|
239
|
-
- a route-level auth guard
|
|
240
|
-
- auth-aware placement predicates
|
|
241
|
-
- a client-side auth composable you can read in your own components
|
|
242
|
-
|
|
243
|
-
Those are different tools, and they do different jobs.
|
|
244
|
-
|
|
245
|
-
- A route guard protects a URL.
|
|
246
|
-
- A placement predicate controls whether a menu entry or widget is visible.
|
|
247
|
-
- The auth composable lets your component react to the current session state.
|
|
248
|
-
|
|
249
|
-
That separation matters. Protecting a route does **not** automatically hide a menu entry, and hiding a menu entry does **not** protect a route.
|
|
250
|
-
|
|
251
|
-
### Start with a normal public page
|
|
252
|
-
|
|
253
|
-
Generate a simple page under the public `home` surface:
|
|
14
|
+
For Supabase authentication:
|
|
254
15
|
|
|
255
16
|
```bash
|
|
256
|
-
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
At this point the page is still public, because `home` is a public surface. JSKIT creates:
|
|
260
|
-
|
|
261
|
-
- `src/pages/home/reports/index.vue`
|
|
262
|
-
- a new menu placement in `src/placement.js`
|
|
263
|
-
|
|
264
|
-
The placement entry is just a normal shell link:
|
|
265
|
-
|
|
266
|
-
```js
|
|
267
|
-
addPlacement({
|
|
268
|
-
id: "ui-generator.page.home.reports.link",
|
|
269
|
-
target: "shell.primary-nav",
|
|
270
|
-
kind: "link",
|
|
271
|
-
surfaces: ["home"],
|
|
272
|
-
order: 155,
|
|
273
|
-
props: {
|
|
274
|
-
label: "Reports",
|
|
275
|
-
surface: "home",
|
|
276
|
-
scopedSuffix: "/reports",
|
|
277
|
-
unscopedSuffix: "/reports"
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
And the page file itself is a normal generated empty-state screen:
|
|
283
|
-
|
|
284
|
-
```vue
|
|
285
|
-
<template>
|
|
286
|
-
<section class="generated-page-screen d-flex flex-column ga-4">
|
|
287
|
-
<header>
|
|
288
|
-
<p class="text-overline text-medium-emphasis mb-1">Screen</p>
|
|
289
|
-
<h1 class="generated-page-screen__title">Reports</h1>
|
|
290
|
-
</header>
|
|
291
|
-
|
|
292
|
-
<v-sheet rounded="lg" border class="generated-page-screen__empty-state">
|
|
293
|
-
<h2 class="text-h6 mb-2">No Reports activity yet</h2>
|
|
294
|
-
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
295
|
-
Activity and actions for this screen will appear here.
|
|
296
|
-
</p>
|
|
297
|
-
</v-sheet>
|
|
298
|
-
</section>
|
|
299
|
-
</template>
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
So immediately after generation:
|
|
303
|
-
|
|
304
|
-
- the `Reports` menu entry is visible to everyone
|
|
305
|
-
- `/home/reports` is reachable by everyone
|
|
306
|
-
|
|
307
|
-
### Gate the page behind login
|
|
308
|
-
|
|
309
|
-
To make the route require login, add a route guard block to `src/pages/home/reports/index.vue`:
|
|
310
|
-
|
|
311
|
-
```vue
|
|
312
|
-
<route lang="json">
|
|
313
|
-
{
|
|
314
|
-
"meta": {
|
|
315
|
-
"guard": {
|
|
316
|
-
"policy": "authenticated"
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
</route>
|
|
321
|
-
|
|
322
|
-
<template>
|
|
323
|
-
<section class="generated-page-screen d-flex flex-column ga-4">
|
|
324
|
-
<header>
|
|
325
|
-
<p class="text-overline text-medium-emphasis mb-1">Screen</p>
|
|
326
|
-
<h1 class="generated-page-screen__title">Reports</h1>
|
|
327
|
-
</header>
|
|
328
|
-
|
|
329
|
-
<v-sheet rounded="lg" border class="generated-page-screen__empty-state">
|
|
330
|
-
<h2 class="text-h6 mb-2">No Reports activity yet</h2>
|
|
331
|
-
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
332
|
-
Activity and actions for this screen will appear here.
|
|
333
|
-
</p>
|
|
334
|
-
</v-sheet>
|
|
335
|
-
</section>
|
|
336
|
-
</template>
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
That one change protects the route itself. If a signed-out user tries to visit `/home/reports`, the auth guard runtime redirects them to the login route instead of letting the page render.
|
|
340
|
-
|
|
341
|
-
The redirect also keeps the requested target. In practice the browser ends up on a login URL shaped like this:
|
|
342
|
-
|
|
343
|
-
```text
|
|
344
|
-
/auth/login?returnTo=%2Fhome%2Freports
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
So after login, JSKIT can send the user back to the page they originally asked for.
|
|
348
|
-
|
|
349
|
-
### Hide the menu entry when signed out
|
|
350
|
-
|
|
351
|
-
The route is protected, but the drawer link is still visible. That is expected. Route protection and shell visibility are separate concerns.
|
|
352
|
-
|
|
353
|
-
To hide the `Reports` menu entry until the user is logged in, update the placement entry in `src/placement.js`:
|
|
354
|
-
|
|
355
|
-
```js
|
|
356
|
-
addPlacement({
|
|
357
|
-
id: "ui-generator.page.home.reports.link",
|
|
358
|
-
target: "shell.primary-nav",
|
|
359
|
-
kind: "link",
|
|
360
|
-
surfaces: ["home"],
|
|
361
|
-
order: 155,
|
|
362
|
-
props: {
|
|
363
|
-
label: "Reports",
|
|
364
|
-
surface: "home",
|
|
365
|
-
scopedSuffix: "/reports",
|
|
366
|
-
unscopedSuffix: "/reports"
|
|
367
|
-
},
|
|
368
|
-
// Added: only show this menu entry when the current auth context is authenticated.
|
|
369
|
-
when: ({ auth }) => Boolean(auth?.authenticated)
|
|
370
|
-
});
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
The only new part is the `when(...)` line. That predicate is evaluated by the shell placement runtime using the auth context that `auth-web` injects from `/api/session`.
|
|
374
|
-
|
|
375
|
-
So the behavior becomes:
|
|
376
|
-
|
|
377
|
-
- signed out:
|
|
378
|
-
- the `Reports` drawer entry disappears
|
|
379
|
-
- visiting `/home/reports` manually still redirects to `/auth/login`
|
|
380
|
-
- signed in:
|
|
381
|
-
- the `Reports` drawer entry appears
|
|
382
|
-
- `/home/reports` renders normally
|
|
383
|
-
|
|
384
|
-
This is the most important pattern to understand: use the guard to protect the route, and use the placement `when(...)` function to control whether the shell exposes a link to it.
|
|
385
|
-
|
|
386
|
-
### Read auth state in your own page code
|
|
387
|
-
|
|
388
|
-
Sometimes you do not want to redirect or hide a menu entry. You just want the page to react differently when a user is logged in.
|
|
389
|
-
|
|
390
|
-
For that, use `useAuthStore()` from `auth-web`. By this point in the guide the scaffold already has Pinia installed from day 0, and `shell-web` has already shown the same store-facing pattern for shell UI state. `auth-web` adds the auth version of that pattern: a Pinia store facade over the underlying auth runtime, so normal Vue code can read the session state without manually wiring subscriptions.
|
|
391
|
-
|
|
392
|
-
Here is a small example that changes `src/pages/home/index.vue` so it shows a success message when the session is authenticated:
|
|
393
|
-
|
|
394
|
-
```vue
|
|
395
|
-
<script setup>
|
|
396
|
-
import { useAuthStore } from "@jskit-ai/auth-web/client";
|
|
397
|
-
|
|
398
|
-
const auth = useAuthStore();
|
|
399
|
-
</script>
|
|
400
|
-
|
|
401
|
-
<template>
|
|
402
|
-
<section class="pa-4">
|
|
403
|
-
<v-alert v-if="auth.authenticated" type="success" variant="tonal" class="mb-4">
|
|
404
|
-
You are logged in!
|
|
405
|
-
</v-alert>
|
|
406
|
-
|
|
407
|
-
<h1 class="text-h5 mb-2">Home</h1>
|
|
408
|
-
<p class="text-body-2 text-medium-emphasis">Your account session is active on this device.</p>
|
|
409
|
-
</section>
|
|
410
|
-
</template>
|
|
17
|
+
npm install @jskit-ai/auth-provider-supabase-core @jskit-ai/auth-web
|
|
411
18
|
```
|
|
412
19
|
|
|
413
|
-
The
|
|
414
|
-
|
|
415
|
-
`useAuthStore()` also gives you the rest of the surfaced auth state and the lower-level runtime methods when you need them:
|
|
416
|
-
|
|
417
|
-
- `authState`
|
|
418
|
-
- `authenticated`
|
|
419
|
-
- `username`
|
|
420
|
-
- `oauthProviders`
|
|
421
|
-
- `oauthDefaultProvider`
|
|
422
|
-
- `initialize()`
|
|
423
|
-
- `refresh()`
|
|
424
|
-
- `getState()`
|
|
425
|
-
- `subscribe()`
|
|
426
|
-
- `runtime`
|
|
427
|
-
|
|
428
|
-
If you need one of those methods, keep the whole auth object instead of only destructuring a single ref:
|
|
429
|
-
|
|
430
|
-
```vue
|
|
431
|
-
<script setup>
|
|
432
|
-
import { useAuthStore } from "@jskit-ai/auth-web/client";
|
|
433
|
-
|
|
434
|
-
const auth = useAuthStore();
|
|
435
|
-
|
|
436
|
-
async function refreshSession() {
|
|
437
|
-
await auth.refresh();
|
|
438
|
-
console.log(auth.getState());
|
|
439
|
-
}
|
|
440
|
-
</script>
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
So this is not just useful for a demo banner. It is the same mechanism you would use for:
|
|
444
|
-
|
|
445
|
-
- guest vs authenticated copy
|
|
446
|
-
- showing a call-to-action only for signed-out users
|
|
447
|
-
- enabling a tool panel only for authenticated users
|
|
448
|
-
- rendering a user-specific welcome message
|
|
449
|
-
|
|
450
|
-
### The three auth tools, side by side
|
|
451
|
-
|
|
452
|
-
By this point the surfaced auth API should be clearer:
|
|
453
|
-
|
|
454
|
-
- route file meta:
|
|
455
|
-
- use `"policy": "authenticated"` when the page itself must be protected
|
|
456
|
-
- placement entry:
|
|
457
|
-
- use `when: ({ auth }) => Boolean(auth?.authenticated)` when shell UI should only appear for signed-in users
|
|
458
|
-
- component code:
|
|
459
|
-
- use `useAuthStore()` when the page needs to react to auth state directly
|
|
460
|
-
|
|
461
|
-
That is the real development payoff of this chapter. The login system is not just a screen. It gives the app a reusable auth state model that routing, shell placements, and component code can all use.
|
|
462
|
-
|
|
463
|
-
At this point the guide has shown three distinct layers of client state:
|
|
464
|
-
|
|
465
|
-
- the scaffold installs Pinia but does not expose any package stores yet
|
|
466
|
-
- `shell-web` adds shell-facing stores such as `useShellLayoutStore()`
|
|
467
|
-
- `auth-web` adds `useAuthStore()` for authentication state
|
|
468
|
-
|
|
469
|
-
That progression is intentional. Packages keep their operational runtimes internally, but the app-facing shared state they surface to Vue code is store-based.
|
|
470
|
-
|
|
471
|
-
## What the local auth install adds to the app
|
|
472
|
-
|
|
473
|
-
The interesting part of this chapter is that authentication appears in several different layers at once: environment config, public routing config, shell placements, and app-owned view wrappers.
|
|
474
|
-
|
|
475
|
-
The first new place to inspect is `package.json`:
|
|
476
|
-
|
|
477
|
-
```json
|
|
478
|
-
{
|
|
479
|
-
"scripts": {
|
|
480
|
-
"server:auth": "SERVER_SURFACE=auth node ./bin/server.js",
|
|
481
|
-
"dev:auth": "VITE_SURFACE=auth vite",
|
|
482
|
-
"build:auth": "VITE_SURFACE=auth vite build"
|
|
483
|
-
},
|
|
484
|
-
"dependencies": {
|
|
485
|
-
"@jskit-ai/auth-core": "0.1.146",
|
|
486
|
-
"@jskit-ai/auth-provider-local-core": "0.1.49",
|
|
487
|
-
"@jskit-ai/auth-web": "0.1.148"
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
Four things are worth noticing immediately.
|
|
493
|
-
|
|
494
|
-
- `auth-core` owns the provider-neutral contract, shared actions, policy hooks, and capability normalization.
|
|
495
|
-
- `auth-provider-local-core` is the selected provider-specific runtime.
|
|
496
|
-
- `auth-web` is the part that adds the web routes and the default auth UI.
|
|
497
|
-
- there is an `auth` surface-specific dev/build script family, just as `home` already had.
|
|
498
|
-
|
|
499
|
-
The provider package also writes local auth settings into `.env`:
|
|
500
|
-
|
|
501
|
-
```dotenv
|
|
502
|
-
AUTH_PROVIDER=local
|
|
503
|
-
AUTH_LOCAL_BACKEND=file
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
`AUTH_PROVIDER=local` is the selected-provider marker. `AUTH_LOCAL_BACKEND=file` tells the local provider to use its built-in file backend. No database URL, Supabase URL, or provider key is needed for this chapter.
|
|
507
|
-
|
|
508
|
-
The backend selection is intentionally local-provider-specific. `auth.local.backend` is the public server container token for replacing the file store later; Supabase and other external providers do not use it. A custom local backend must expose a single `withTransaction(callback)` method. JSKIT calls that method for register, login, recovery, password reset, profile update, session revocation, and security operations, and the callback receives transactional `users`, `sessions`, and `recovery` repositories.
|
|
509
|
-
|
|
510
|
-
The built-in file backend is for low-friction local development, first deploys with one persistent Node instance, and simple apps that accept that storage model. It writes `.jskit/auth/*.passwd` files behind a store-level lock. Do not treat it as horizontally scalable or serverless-safe storage; when the app needs replicated auth storage, install the DB-backed local auth package or register a custom `auth.local.backend`.
|
|
511
|
-
|
|
512
|
-
That DB-backed local auth storage is not the same thing as the `users-core` profile tables. `users-core` creates app-owned `users` and `user_settings` rows later. DB-backed local auth stores the provider-owned auth state that the file backend would otherwise keep in `.jskit/auth/`.
|
|
513
|
-
|
|
514
|
-
The batteries-included DB package is `@jskit-ai/auth-provider-local-db-core`. It uses JSKIT's database runtime and owns three auth-local tables:
|
|
515
|
-
|
|
516
|
-
- `auth_local_users` for provider auth identities: normalized email, display name, disabled flag, password algorithm/version/salt/hash, and timestamps.
|
|
517
|
-
- `auth_local_sessions` for refresh sessions: session id, auth-local user id, refresh-token hash, session purpose such as `normal` or `recovery`, expiry, revocation, and timestamps.
|
|
518
|
-
- `auth_local_recovery` for password recovery: recovery token id, auth-local user id, recovery-token hash, expiry, used-at timestamp, and timestamps.
|
|
519
|
-
|
|
520
|
-
To use it, install a JSKIT database runtime and driver, install `auth-provider-local-db-core`, run migrations, and use `AUTH_LOCAL_BACKEND=db`. The package registers `auth.local.backend` for DB mode. If `AUTH_LOCAL_BACKEND=db` is set without the DB backend package, local auth fails instead of falling back to file storage.
|
|
521
|
-
|
|
522
|
-
**Summary: Adding Database-Backed Users**
|
|
523
|
-
|
|
524
|
-
There are two database-backed paths, and they solve different problems.
|
|
525
|
-
|
|
526
|
-
For normal app users and account settings, install the database runtime, install `users-web`, then run the migrations. That gives the app the `users` and `user_settings` tables through `users-core`, plus the `auth.profile.projector` service that projects a provider identity into a persistent JSKIT user profile.
|
|
527
|
-
|
|
528
|
-
For local auth credentials and sessions in SQL, keep `AUTH_PROVIDER=local`, install `auth-provider-local-db-core`, set `AUTH_LOCAL_BACKEND=db`, and run migrations. That package owns `auth_local_users`, `auth_local_sessions`, and `auth_local_recovery`, and registers the transactional `auth.local.backend` implementation.
|
|
529
|
-
|
|
530
|
-
The first path makes authenticated people real JSKIT users. The second path moves local auth's passwords, refresh sessions, and recovery tokens out of `.jskit/auth/` and into the database. Many apps need the first path before they need the second.
|
|
531
|
-
|
|
532
|
-
Apps that need audit logs, login counters, MFA records, or password history should add sidecar tables that reference `auth_local_users.id`; do not edit the package-owned auth-local migrations or add app-specific columns to those tables. Apps with different table names, external KMS, non-SQL storage, or unusual tenancy partitioning should replace the backend by registering their own `auth.local.backend`.
|
|
533
|
-
|
|
534
|
-
Local auth also applies the shared auth-service decorator registry from `auth-core`. Use that registry when an app or package needs to wrap provider behavior without owning the local auth tables. The local package exports a narrow helper for after-register work:
|
|
535
|
-
|
|
536
|
-
```js
|
|
537
|
-
import { registerAuthServiceDecorator } from "@jskit-ai/auth-core/server/authServiceDecoratorRegistry";
|
|
538
|
-
import { createLocalAuthRegisterHookDecorator } from "@jskit-ai/auth-provider-local-core/server/lib/index";
|
|
539
|
-
|
|
540
|
-
registerAuthServiceDecorator(app, "app.auth.local.permissions", (scope) => {
|
|
541
|
-
const permissionsService = scope.make("permissionsService");
|
|
542
|
-
|
|
543
|
-
return createLocalAuthRegisterHookDecorator({
|
|
544
|
-
decoratorId: "app.auth.local.permissions",
|
|
545
|
-
order: 10,
|
|
546
|
-
hook: {
|
|
547
|
-
hookId: "permissions",
|
|
548
|
-
blocking: true,
|
|
549
|
-
async handle({ actor, profile }) {
|
|
550
|
-
await permissionsService.createDefaultsForUser({
|
|
551
|
-
userId: actor.id,
|
|
552
|
-
email: actor.email,
|
|
553
|
-
displayName: profile.displayName
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
The hook must declare `blocking: true` or `blocking: false`. A blocking hook is awaited and its failure rejects the register call, which fits permission provisioning or other work the app needs before treating registration as complete. A non-blocking hook is scheduled after registration and logs failures, which fits audit logging or metrics. This helper runs after local auth registration and profile projection have succeeded; it does not share the local auth backend transaction. If the extension must be transaction-coupled with credential storage, wrap or replace `auth.local.backend` instead.
|
|
562
|
-
|
|
563
|
-
The local provider also has a separate password strategy seam. Use `auth.local.backend` to change where local auth records are stored. Use `auth.local.passwordStrategy` only when the app owns a different password-record format or verification algorithm.
|
|
564
|
-
|
|
565
|
-
Register `auth.local.passwordStrategy` before `AuthLocalServiceProvider` starts:
|
|
566
|
-
|
|
567
|
-
```js
|
|
568
|
-
import bcrypt from "bcryptjs";
|
|
569
|
-
import { verifyPassword as verifyScryptPassword } from "@jskit-ai/auth-provider-local-core/server/lib/index";
|
|
570
|
-
|
|
571
|
-
app.singleton("auth.local.passwordStrategy", () => ({
|
|
572
|
-
async verifyPassword(password, record) {
|
|
573
|
-
if (typeof record === "string" && /^\$2[aby]\$/.test(record)) {
|
|
574
|
-
return bcrypt.compare(password, record);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
return verifyScryptPassword(password, record);
|
|
578
|
-
}
|
|
579
|
-
}));
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
The strategy object can provide `hashPassword(password)`, `verifyPassword(password, storedPasswordRecord)`, or both. Missing methods use the local provider's default scrypt implementation. JSKIT validates provided methods as functions and calls them with `this` bound to the strategy object.
|
|
583
|
-
|
|
584
|
-
Custom service callers can pass the same object directly:
|
|
585
|
-
|
|
586
|
-
```js
|
|
587
|
-
createLocalAuthService({
|
|
588
|
-
backend,
|
|
589
|
-
config,
|
|
590
|
-
passwordStrategy
|
|
591
|
-
});
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
JSKIT core does not install bcrypt. An app that selects bcrypt owns that dependency and keeps the backend focused on storage.
|
|
595
|
-
|
|
596
|
-
If you later configure local SMTP password recovery, also set `APP_PUBLIC_URL` so reset links point back to the browser URL for this app.
|
|
597
|
-
|
|
598
|
-
Public routing config changes too. `config/public.js` has a second surface:
|
|
599
|
-
|
|
600
|
-
```js
|
|
601
|
-
config.surfaceDefinitions.auth = {
|
|
602
|
-
id: "auth",
|
|
603
|
-
label: "Auth",
|
|
604
|
-
pagesRoot: "auth",
|
|
605
|
-
enabled: true,
|
|
606
|
-
requiresAuth: false,
|
|
607
|
-
requiresWorkspace: false,
|
|
608
|
-
origin: ""
|
|
609
|
-
};
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
That `requiresAuth: false` line is important. The auth surface must stay public, otherwise users would need to be logged in before they could reach the login page.
|
|
613
|
-
|
|
614
|
-
The local provider does not need an app database profile mode or OAuth config. The stock login UI asks `/api/session` for the active provider capabilities and renders only what the selected provider actually supports. With the local provider, that means email/password login, registration, sign-out, session refresh, and password recovery when recovery is configured.
|
|
615
|
-
|
|
616
|
-
Later, if you switch to Supabase, the Supabase provider can append the app-owned OAuth visibility config and profile mode settings it needs.
|
|
617
|
-
|
|
618
|
-
The auth routes themselves are app-owned wrappers around the module-supplied default views. `src/pages/auth/login.vue` looks like this:
|
|
619
|
-
|
|
620
|
-
```vue
|
|
621
|
-
<route lang="json">
|
|
622
|
-
{
|
|
623
|
-
"meta": {
|
|
624
|
-
"guard": {
|
|
625
|
-
"policy": "public"
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
</route>
|
|
630
|
-
|
|
631
|
-
<script setup>
|
|
632
|
-
import DefaultLoginView from "@jskit-ai/auth-web/client/views/DefaultLoginView";
|
|
633
|
-
</script>
|
|
634
|
-
|
|
635
|
-
<template>
|
|
636
|
-
<DefaultLoginView />
|
|
637
|
-
</template>
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
There are two important ideas in that small file.
|
|
641
|
-
|
|
642
|
-
- The route is explicitly public.
|
|
643
|
-
- The app owns the page wrapper even though the actual stock login form comes from the installed module.
|
|
644
|
-
|
|
645
|
-
The `meta.guard` block is ordinary route metadata, not a special auth-only API. The file-based router plugin turns `<route lang="json">` into a normal Vue Router `meta` object first, and then JSKIT reads `meta.guard.policy` during navigation. That same mechanism is used elsewhere in the app too.
|
|
646
|
-
|
|
647
|
-
That pattern gives you a clean customization seam later. The module supplies a working default, but the app still owns the route file and can replace or wrap the view if needed.
|
|
648
|
-
|
|
649
|
-
Authentication also becomes visible in the shell through `src/placement.js`:
|
|
650
|
-
|
|
651
|
-
```js
|
|
652
|
-
addPlacement({
|
|
653
|
-
id: "auth.profile.widget",
|
|
654
|
-
target: "shell.status",
|
|
655
|
-
kind: "component",
|
|
656
|
-
surfaces: ["*"],
|
|
657
|
-
order: 1000,
|
|
658
|
-
componentToken: "auth.web.profile.widget"
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
addPlacement({
|
|
662
|
-
id: "auth.profile.menu.sign-in",
|
|
663
|
-
target: "auth.profile-menu",
|
|
664
|
-
kind: "link",
|
|
665
|
-
surfaces: ["*"],
|
|
666
|
-
order: 200,
|
|
667
|
-
props: {
|
|
668
|
-
label: "Sign in",
|
|
669
|
-
to: "/auth/login"
|
|
670
|
-
},
|
|
671
|
-
when: ({ auth }) => !Boolean(auth?.authenticated)
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
addPlacement({
|
|
675
|
-
id: "auth.profile.menu.sign-out",
|
|
676
|
-
target: "auth.profile-menu",
|
|
677
|
-
kind: "link",
|
|
678
|
-
surfaces: ["*"],
|
|
679
|
-
order: 1000,
|
|
680
|
-
props: {
|
|
681
|
-
label: "Sign out",
|
|
682
|
-
to: "/auth/signout"
|
|
683
|
-
},
|
|
684
|
-
when: ({ auth }) => Boolean(auth?.authenticated)
|
|
685
|
-
});
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
This is the shell placement system from the previous chapter doing real work again. `auth-web` does not hard-code a permanent auth button into `ShellLayout.vue`. Instead, it contributes the profile widget into the semantic `shell.status` placement and contributes sign-in/sign-out links into the semantic `auth.profile-menu` placement. Topology maps those semantic targets to concrete outlets and renderers.
|
|
689
|
-
|
|
690
|
-
So the auth story in this chapter is spread across clear responsibilities:
|
|
691
|
-
|
|
692
|
-
- `.env` selects the local provider and its backend
|
|
693
|
-
- `config/public.js` declares an `auth` surface
|
|
694
|
-
- `src/pages/auth/*` gives the app real public auth routes
|
|
695
|
-
- `src/placement.js` makes auth visible in the shell
|
|
696
|
-
|
|
697
|
-
That is a very JSKIT-style pattern. The installed package brings the runtime behavior, but the app still owns the important seams where routing and UI get attached.
|
|
698
|
-
|
|
699
|
-
## Under the hood
|
|
700
|
-
|
|
701
|
-
### The runtime behind `useAuthStore()`
|
|
702
|
-
|
|
703
|
-
`useAuthStore()` is the app-facing Pinia layer, but it is not inventing a second auth system. It is a store facade over the lower-level auth guard runtime that `auth-web` boots on startup.
|
|
704
|
-
|
|
705
|
-
That lower-level runtime already has a small, concrete contract:
|
|
706
|
-
|
|
707
|
-
- `initialize()`
|
|
708
|
-
- `refresh()`
|
|
709
|
-
- `getState()`
|
|
710
|
-
- `subscribe()`
|
|
711
|
-
|
|
712
|
-
`auth-web` initializes that runtime once, binds it into the Pinia auth store, and then exposes `useAuthStore()` as the normal component-facing API. That is why the main example earlier could stay so small.
|
|
713
|
-
|
|
714
|
-
If you strip the composable away and write the same `You are logged in!` example directly against the runtime, it looks like this:
|
|
715
|
-
|
|
716
|
-
```vue
|
|
717
|
-
<script setup>
|
|
718
|
-
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
|
719
|
-
import { useAuthGuardRuntime } from "@jskit-ai/auth-web/client";
|
|
720
|
-
|
|
721
|
-
const authGuardRuntime = useAuthGuardRuntime({
|
|
722
|
-
required: true
|
|
723
|
-
});
|
|
724
|
-
const authState = ref(authGuardRuntime.getState());
|
|
725
|
-
let unsubscribe = null;
|
|
726
|
-
|
|
727
|
-
const isAuthenticated = computed(() => authState.value?.authenticated === true);
|
|
728
|
-
|
|
729
|
-
onMounted(() => {
|
|
730
|
-
unsubscribe = authGuardRuntime.subscribe((nextState) => {
|
|
731
|
-
authState.value = nextState;
|
|
732
|
-
});
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
onBeforeUnmount(() => {
|
|
736
|
-
if (typeof unsubscribe === "function") {
|
|
737
|
-
unsubscribe();
|
|
738
|
-
}
|
|
739
|
-
});
|
|
740
|
-
</script>
|
|
741
|
-
|
|
742
|
-
<template>
|
|
743
|
-
<section class="pa-4">
|
|
744
|
-
<v-alert v-if="isAuthenticated" type="success" variant="tonal" class="mb-4">
|
|
745
|
-
You are logged in!
|
|
746
|
-
</v-alert>
|
|
747
|
-
|
|
748
|
-
<h1 class="text-h5 mb-2">Home</h1>
|
|
749
|
-
<p class="text-body-2 text-medium-emphasis">Your account session is active on this device.</p>
|
|
750
|
-
</section>
|
|
751
|
-
</template>
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
That code works, and it shows exactly what `useAuthStore()` is wrapping:
|
|
755
|
-
|
|
756
|
-
- `getState()` gives the first auth snapshot immediately
|
|
757
|
-
- `subscribe(...)` keeps that snapshot updated later
|
|
758
|
-
- the component turns that imperative runtime into normal Vue refs and computeds
|
|
759
|
-
|
|
760
|
-
For ordinary Vue component code there is usually no advantage to writing it this way. `useAuthStore()` already gives you the same surfaced information plus the same runtime methods when you need them. The direct runtime version is mainly worth knowing so you understand the lower-level contract that `auth-web` itself is building on.
|
|
761
|
-
|
|
762
|
-
### Who actually talks to whom
|
|
763
|
-
|
|
764
|
-
The most important thing to understand is that the browser talks to **your app**, and the app talks to the **selected auth provider** through the provider-neutral `authService` contract.
|
|
765
|
-
|
|
766
|
-
With the local provider, the selected provider is in the same Node process. It verifies passwords, writes sessions, and stores local auth state through `auth.local.backend`. The default backend writes the `.jskit/auth/*.passwd` files; `auth-provider-local-db-core` moves that storage into package-owned database tables without changing the login UI or the shared auth routes.
|
|
767
|
-
|
|
768
|
-
If `users-core` or another package registers `auth.profile.projector`, local auth now resolves that projector lazily when it actually builds an auth payload for register, login, session refresh, or request authentication. That keeps local auth boot independent from the projector's own dependencies, such as repositories behind `internal.json-rest-api`.
|
|
769
|
-
|
|
770
|
-
That means there are two actors in the first-app flow:
|
|
771
|
-
|
|
772
|
-
- the browser, which renders the login screen and submits forms
|
|
773
|
-
- the JSKIT app server, which owns `/api/login`, `/api/register`, `/api/password/*`, `/api/session`, the selected provider, and the cookie-writing step
|
|
774
|
-
|
|
775
|
-
So the default local mental model is:
|
|
776
|
-
|
|
777
|
-
```text
|
|
778
|
-
browser -> JSKIT app -> local auth provider
|
|
779
|
-
browser <- JSKIT app <- local auth provider
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
If you later switch to Supabase, the same app routes still exist, but the selected provider implementation delegates to Supabase Auth.
|
|
783
|
-
|
|
784
|
-
### Password login: the full round trip
|
|
785
|
-
|
|
786
|
-
The simplest login flow is the normal `Email + Password` form.
|
|
787
|
-
|
|
788
|
-
On the client side, `DefaultLoginView` eventually calls `useLoginViewActions.submitAuth()`. In sign-in mode that becomes a `POST` to:
|
|
789
|
-
|
|
790
|
-
```text
|
|
791
|
-
/api/login
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
with a body shaped roughly like this:
|
|
795
|
-
|
|
796
|
-
```json
|
|
797
|
-
{
|
|
798
|
-
"email": "alice@example.com",
|
|
799
|
-
"password": "correct horse battery staple"
|
|
800
|
-
}
|
|
801
|
-
```
|
|
802
|
-
|
|
803
|
-
From there, the server-side flow is:
|
|
804
|
-
|
|
805
|
-
1. `POST /api/login` hits the route registered by `auth-web`.
|
|
806
|
-
2. `AuthController.login()` receives the request.
|
|
807
|
-
3. `AuthWebService.login()` executes the internal action `auth.login.password`.
|
|
808
|
-
4. `auth-core` routes that action to the selected provider's `authService.login(...)`.
|
|
809
|
-
5. The local provider checks the password hash in its backend and creates a session record.
|
|
810
|
-
6. JSKIT writes the access and refresh tokens into HTTP-only cookies.
|
|
811
|
-
7. The API response sent back to the browser is intentionally small.
|
|
812
|
-
|
|
813
|
-
The important detail is step 6. The browser does **not** receive raw session tokens as normal application state. The server writes them into HTTP-only cookies instead. With the local provider, those cookies are:
|
|
814
|
-
|
|
815
|
-
- `jskit_local_access_token`
|
|
816
|
-
- `jskit_local_refresh_token`
|
|
817
|
-
|
|
818
|
-
Those cookies are HTTP-only and `sameSite: "lax"`, so the browser sends them back automatically on later requests, but normal client-side code cannot read them directly.
|
|
819
|
-
|
|
820
|
-
The JSON response from `/api/login` stays small:
|
|
821
|
-
|
|
822
|
-
```json
|
|
823
|
-
{
|
|
824
|
-
"ok": true,
|
|
825
|
-
"username": "alice"
|
|
826
|
-
}
|
|
827
|
-
```
|
|
828
|
-
|
|
829
|
-
After that, the browser still is not done. The login view immediately calls `/api/session` to confirm the session and fetch the current auth state. If that session check comes back with `authenticated: true`, the client redirects to the requested `returnTo` path.
|
|
830
|
-
|
|
831
|
-
So the real password-login round trip is:
|
|
832
|
-
|
|
833
|
-
```text
|
|
834
|
-
1. browser -> POST /api/login -> JSKIT app
|
|
835
|
-
2. JSKIT app -> selected auth provider login
|
|
836
|
-
3. selected auth provider -> JSKIT app: profile + session
|
|
837
|
-
4. JSKIT app -> browser: set HTTP-only cookies + { ok, username }
|
|
838
|
-
5. browser -> GET /api/session
|
|
839
|
-
6. JSKIT app -> browser: { authenticated, username, csrfToken, authCapabilities, ... }
|
|
840
|
-
7. browser redirects to the requested route
|
|
841
|
-
```
|
|
842
|
-
|
|
843
|
-
### OAuth login: the extra browser bounce
|
|
844
|
-
|
|
845
|
-
Local auth does not expose OAuth buttons. If you later switch to Supabase and configure OAuth providers, OAuth is the case where the browser really does leave the app briefly, but the app still owns the edges of the flow.
|
|
846
|
-
|
|
847
|
-
The first step is still browser -> app. If the login page shows a button such as `Continue with Google`, clicking it does **not** go straight to Supabase. It first goes to:
|
|
848
|
-
|
|
849
|
-
```text
|
|
850
|
-
/api/oauth/google/start?returnTo=/home
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
That server route does three jobs:
|
|
854
|
-
|
|
855
|
-
- normalizes the provider id
|
|
856
|
-
- normalizes the `returnTo` path
|
|
857
|
-
- asks Supabase for the correct provider redirect URL
|
|
858
|
-
|
|
859
|
-
JSKIT then redirects the browser to Supabase's OAuth flow. The redirect URL that JSKIT asks Supabase to use points back to your app, usually `/auth/login`, with some query parameters describing the provider and the intended return target.
|
|
860
|
-
|
|
861
|
-
So the browser flow becomes:
|
|
862
|
-
|
|
863
|
-
```text
|
|
864
|
-
browser -> /api/oauth/google/start
|
|
865
|
-
app -> Supabase OAuth redirect URL
|
|
866
|
-
browser -> Supabase / provider login page
|
|
867
|
-
Supabase -> browser back to /auth/login?...callback params...
|
|
868
|
-
```
|
|
869
|
-
|
|
870
|
-
When the browser lands back on `/auth/login`, the login page JavaScript inspects the URL. It looks for either:
|
|
871
|
-
|
|
872
|
-
- an OAuth `code`, or
|
|
873
|
-
- an access/refresh token pair
|
|
874
|
-
|
|
875
|
-
If it finds them, it does **not** treat the browser as fully signed in yet. Instead, it posts a small completion payload back to the app at:
|
|
876
|
-
|
|
877
|
-
```text
|
|
878
|
-
/api/oauth/complete
|
|
879
|
-
```
|
|
880
|
-
|
|
881
|
-
That payload looks roughly like one of these:
|
|
882
|
-
|
|
883
|
-
```json
|
|
884
|
-
{
|
|
885
|
-
"provider": "google",
|
|
886
|
-
"code": "..."
|
|
887
|
-
}
|
|
888
|
-
```
|
|
889
|
-
|
|
890
|
-
or
|
|
891
|
-
|
|
892
|
-
```json
|
|
893
|
-
{
|
|
894
|
-
"provider": "google",
|
|
895
|
-
"accessToken": "...",
|
|
896
|
-
"refreshToken": "..."
|
|
897
|
-
}
|
|
898
|
-
```
|
|
899
|
-
|
|
900
|
-
Now the app server finishes the job:
|
|
901
|
-
|
|
902
|
-
1. `AuthController.oauthComplete()` receives the payload.
|
|
903
|
-
2. The Supabase auth service either:
|
|
904
|
-
- exchanges the code with `supabase.auth.exchangeCodeForSession(...)`, or
|
|
905
|
-
- restores the session with `supabase.auth.setSession(...)`
|
|
906
|
-
3. Supabase returns `user` and `session`.
|
|
907
|
-
4. JSKIT syncs the local profile mirror.
|
|
908
|
-
5. JSKIT writes HTTP-only cookies.
|
|
909
|
-
6. The browser strips the callback params out of the URL.
|
|
910
|
-
7. The browser refreshes `/api/session`.
|
|
911
|
-
8. The browser redirects to `returnTo`.
|
|
912
|
-
|
|
913
|
-
So the full OAuth dance is:
|
|
914
|
-
|
|
915
|
-
```text
|
|
916
|
-
browser -> app start route
|
|
917
|
-
app -> Supabase redirect
|
|
918
|
-
browser -> Supabase/provider
|
|
919
|
-
Supabase/provider -> browser back to /auth/login
|
|
920
|
-
browser -> app completion route
|
|
921
|
-
app -> Supabase session exchange
|
|
922
|
-
Supabase -> app: user + session
|
|
923
|
-
app -> browser: cookies + small success payload
|
|
924
|
-
browser -> /api/session -> redirect
|
|
925
|
-
```
|
|
926
|
-
|
|
927
|
-
That is why the login page needs both browser-side logic and server-side routes. The browser owns the redirect dance, but the app still owns the final session establishment step.
|
|
928
|
-
|
|
929
|
-
### What `/api/session` is really doing
|
|
930
|
-
|
|
931
|
-
`/api/session` is more than a yes-or-no login check. It is the app's current auth truth endpoint.
|
|
932
|
-
|
|
933
|
-
When the browser calls it, the server:
|
|
934
|
-
|
|
935
|
-
- reads the auth cookies
|
|
936
|
-
- asks the selected provider to authenticate the request
|
|
937
|
-
- refreshes or reissues provider cookies when the provider returns a refreshed session
|
|
938
|
-
- clears invalid cookies if the session is no longer usable
|
|
939
|
-
- returns the auth state the client actually needs
|
|
940
|
-
|
|
941
|
-
The response is shaped roughly like this:
|
|
942
|
-
|
|
943
|
-
```json
|
|
944
|
-
{
|
|
945
|
-
"authenticated": true,
|
|
946
|
-
"username": "alice",
|
|
947
|
-
"csrfToken": "...",
|
|
948
|
-
"authCapabilities": { "...": "..." },
|
|
949
|
-
"oauthProviders": [],
|
|
950
|
-
"oauthDefaultProvider": null
|
|
951
|
-
}
|
|
952
|
-
```
|
|
953
|
-
|
|
954
|
-
That explains why the login screen and auth guard runtime both care about `/api/session`. It is how the browser learns:
|
|
955
|
-
|
|
956
|
-
- whether the user is authenticated
|
|
957
|
-
- which username to show
|
|
958
|
-
- which OAuth buttons to render
|
|
959
|
-
- which CSRF token to use for later writes
|
|
960
|
-
|
|
961
|
-
It is also why the shell widget can react cleanly to auth state without storing raw session tokens in client state. The browser just asks the app for the current session view, and the app derives that from its cookies plus the selected provider.
|
|
962
|
-
|
|
963
|
-
### Authenticated Playwright testing
|
|
964
|
-
|
|
965
|
-
JSKIT supports two intentionally separate authenticated-browser paths:
|
|
966
|
-
|
|
967
|
-
- a private localhost exchange for direct local Playwright runs
|
|
968
|
-
- runner-provided Playwright storage state for managed previews
|
|
969
|
-
|
|
970
|
-
Neither path drives a live external login provider. Both put the resulting HTTP-only cookies into the same Playwright browser context that runs the feature assertions.
|
|
971
|
-
|
|
972
|
-
The generated `playwright.config.mjs` is a thin delegate to `@jskit-ai/jskit-cli/test/playwright`. The published helper owns the changing setup behavior:
|
|
973
|
-
|
|
974
|
-
- without `PLAYWRIGHT_BASE_URL`, it builds the app and starts the local server on `http://127.0.0.1:4173`
|
|
975
|
-
- with `PLAYWRIGHT_BASE_URL`, it uses that managed preview and does not start another server
|
|
976
|
-
- with `VIBE64_PLAYWRIGHT_STORAGE_STATE`, it loads the Vibe64-supplied authenticated state into Playwright contexts
|
|
977
|
-
|
|
978
|
-
Tests should therefore navigate with relative paths such as `page.goto("/w/acme/admin/contacts")`.
|
|
979
|
-
|
|
980
|
-
#### Direct local dev-auth login
|
|
981
|
-
|
|
982
|
-
The local dev auth bypass remains deliberately narrow:
|
|
983
|
-
|
|
984
|
-
- it is available only outside production
|
|
985
|
-
- the request must arrive locally
|
|
986
|
-
- it selects an existing user and never creates one
|
|
987
|
-
- it requires both CSRF protection and a private exchange secret
|
|
988
|
-
|
|
989
|
-
Enable it for the Playwright process and the local server it starts:
|
|
990
|
-
|
|
991
|
-
```bash
|
|
992
|
-
AUTH_DEV_BYPASS_ENABLED=true
|
|
993
|
-
AUTH_DEV_BYPASS_SECRET=replace-this-with-a-local-dev-secret
|
|
994
|
-
```
|
|
995
|
-
|
|
996
|
-
When enabled, the app exposes `POST /api/dev-auth/login-as`. The body contains exactly one existing-user identity:
|
|
997
|
-
|
|
998
|
-
```json
|
|
999
|
-
{ "userId": "7" }
|
|
1000
|
-
```
|
|
1001
|
-
|
|
1002
|
-
or:
|
|
1003
|
-
|
|
1004
|
-
```json
|
|
1005
|
-
{ "email": "ada@example.com" }
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
Do not call that route from `page.evaluate()`. The route requires the private `x-jskit-dev-auth-secret` header as well as the CSRF token. Putting the secret into browser JavaScript, client-visible environment, query parameters, or page globals would defeat the exchange boundary.
|
|
1009
|
-
|
|
1010
|
-
Use the published Node-side helper instead:
|
|
1011
|
-
|
|
1012
|
-
```ts
|
|
1013
|
-
import { expect, test } from "@playwright/test";
|
|
1014
|
-
import { loginAsExistingUser } from "@jskit-ai/auth-web/test/playwright";
|
|
1015
|
-
|
|
1016
|
-
test("authenticated contacts filters", async ({ page }) => {
|
|
1017
|
-
await loginAsExistingUser(page, { email: "ada@example.com" });
|
|
1018
|
-
|
|
1019
|
-
await page.goto("/w/acme/admin/contacts");
|
|
1020
|
-
await expect(page.getByRole("heading", { name: "Contacts" })).toBeVisible();
|
|
1021
|
-
});
|
|
1022
|
-
```
|
|
1023
|
-
|
|
1024
|
-
`loginAsExistingUser()` runs from the Playwright Node process. It uses the request client attached to `page.context()`, reads `csrfToken` from `GET /api/session`, sends that token plus `x-jskit-dev-auth-secret` to the login route, and leaves the response cookies in the same browser context. It reads `AUTH_DEV_BYPASS_SECRET` from the Node process by default and refuses to send it to a non-local URL.
|
|
1025
|
-
|
|
1026
|
-
That last restriction is important. A managed preview URL is not a direct localhost app connection, even when its backend eventually runs on the same infrastructure. Project code must not receive the managed host's private exchange authority.
|
|
1027
|
-
|
|
1028
|
-
#### Managed preview authentication
|
|
1029
|
-
|
|
1030
|
-
A managed host authenticates outside the project browser context. It performs its trusted identity exchange, writes the resulting cookies and origins to a temporary Playwright storage-state file, and starts the project test with:
|
|
1031
|
-
|
|
1032
|
-
```bash
|
|
1033
|
-
PLAYWRIGHT_BASE_URL=https://managed-preview.example.test \
|
|
1034
|
-
VIBE64_PLAYWRIGHT_STORAGE_STATE=/secure/temp/playwright-state.json \
|
|
1035
|
-
playwright test tests/e2e/contacts.spec.ts
|
|
1036
|
-
```
|
|
1037
|
-
|
|
1038
|
-
JSKIT only consumes that runner-neutral contract. It does not detect a particular host, call host-specific commands, proxy private headers, or install a browser.
|
|
1039
|
-
|
|
1040
|
-
The storage-state file is a secret because it can contain authenticated cookies. The managed runner must create it with appropriately restricted access, must not print or commit it, and must delete it after the run.
|
|
1041
|
-
|
|
1042
|
-
Tests using managed state do not call `loginAsExistingUser()`. They begin with the runner-provided identity already present and navigate using relative paths. An ordinary browser or proxy request to `/api/dev-auth/login-as` without the private header must continue to fail with `403 Dev auth exchange is not authorized.`
|
|
1043
|
-
|
|
1044
|
-
#### Running the verification
|
|
1045
|
-
|
|
1046
|
-
Run the focused Playwright flow directly:
|
|
1047
|
-
|
|
1048
|
-
```bash
|
|
1049
|
-
npx playwright test tests/e2e/contacts.spec.ts -g filters
|
|
1050
|
-
```
|
|
1051
|
-
|
|
1052
|
-
For local pre-merge review, follow the focused run with:
|
|
1053
|
-
|
|
1054
|
-
```bash
|
|
1055
|
-
npx jskit doctor --against origin/main
|
|
1056
|
-
```
|
|
1057
|
-
|
|
1058
|
-
This keeps feature tests focused on the changed UI while preserving the security boundary around session creation.
|
|
1059
|
-
|
|
1060
|
-
## When you later switch to Supabase
|
|
1061
|
-
|
|
1062
|
-
This chapter does not create anything in Supabase. If you later switch the selected provider from local auth to Supabase, keep separating **Supabase auth data** from **JSKIT app-owned data**.
|
|
1063
|
-
|
|
1064
|
-
When a user registers through a Supabase-backed app, Supabase creates a real auth user. According to Supabase's user-management docs, you can see users in two places.
|
|
1065
|
-
|
|
1066
|
-
- `Authentication -> Users` in the Supabase dashboard
|
|
1067
|
-
- the `auth` schema in the Table Editor
|
|
20
|
+
The dependency graph supplies `@jskit-ai/auth-core`. Do not install two
|
|
21
|
+
providers for the same `auth.service` capability.
|
|
1068
22
|
|
|
1069
|
-
|
|
23
|
+
## Provider ownership
|
|
1070
24
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
-
|
|
1074
|
-
- The user has a stable auth id.
|
|
1075
|
-
- The provider is Supabase email/password unless you later add OAuth.
|
|
25
|
+
The selected provider owns identity verification and sessions. The application
|
|
26
|
+
owns permitted login methods, visible OAuth providers, public routes,
|
|
27
|
+
post-login destinations, surface access policy, and secret injection.
|
|
1076
28
|
|
|
1077
|
-
|
|
29
|
+
Use the `auth/auth-surface` pattern for login, sign-out, reset, guards, profile
|
|
30
|
+
placements, and HTTP integration. Use `auth/supabase-auth` for the Supabase
|
|
31
|
+
environment and configuration shape.
|
|
1078
32
|
|
|
1079
|
-
|
|
33
|
+
## Server boundary
|
|
1080
34
|
|
|
1081
|
-
|
|
35
|
+
`@jskit-ai/auth-core` exposes the policy, actor, extension, and action contracts.
|
|
36
|
+
Protected server operations declare an auth policy; they do not trust a hidden
|
|
37
|
+
button or a page-local session check. Other packages extend auth through the
|
|
38
|
+
public extension capability rather than replacing the auth service.
|
|
1082
39
|
|
|
1083
|
-
|
|
1084
|
-
- JSKIT app-owned profile/settings rows are separate and only exist after you install the users/database layer.
|
|
40
|
+
## Browser boundary
|
|
1085
41
|
|
|
1086
|
-
|
|
42
|
+
`@jskit-ai/auth-web` owns the normal auth views, guard runtime, sign-out flow,
|
|
43
|
+
and auth-aware HTTP integration. Applications may wrap and style those views
|
|
44
|
+
while retaining the behavior contract.
|
|
1087
45
|
|
|
1088
|
-
|
|
46
|
+
Initial load errors stay inside the affected surface. Failed login, reset, and
|
|
47
|
+
other transient mutations use the application toast and do not shift the form.
|
|
48
|
+
Loading uses skeletons when content geometry is known.
|
|
1089
49
|
|
|
1090
|
-
|
|
1091
|
-
- If confirmation is disabled, the user appears in Supabase and gets an active session immediately.
|
|
50
|
+
## Verification
|
|
1092
51
|
|
|
1093
|
-
|
|
52
|
+
Test successful and failed registration/login, sign-out, password reset,
|
|
53
|
+
protected direct navigation, return destinations, refresh, expired sessions,
|
|
54
|
+
and negative cross-origin behavior. For Supabase, also exercise callback and
|
|
55
|
+
refresh flows.
|
|
1094
56
|
|
|
1095
|
-
|
|
57
|
+
For direct localhost Playwright tests, enable the development auth bypass only
|
|
58
|
+
in the server process and use `loginAsExistingUser()` from
|
|
59
|
+
`@jskit-ai/auth-web/test/playwright`. The Node-side helper sends the private
|
|
60
|
+
`x-jskit-dev-auth-secret`; never pass that secret through browser code, URLs, or
|
|
61
|
+
client environment.
|
|
1096
62
|
|
|
1097
|
-
|
|
63
|
+
A managed host instead supplies authenticated state through
|
|
64
|
+
`VIBE64_PLAYWRIGHT_STORAGE_STATE` and the app-owned Playwright config. Treat the
|
|
65
|
+
file as a temporary secret, use relative URLs, and do not call the localhost
|
|
66
|
+
login helper or start a duplicate server.
|
|
1098
67
|
|
|
1099
|
-
|
|
68
|
+
Never commit provider secrets, copy auth repositories into the app, or add an
|
|
69
|
+
installation questionnaire, mutation record, or setup receipt.
|