@lunora/cli 1.0.0-alpha.39 → 1.0.0-alpha.390
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/LICENSE.md +33 -0
- package/README.md +1 -1
- package/dist/bin.mjs +2 -10
- package/dist/index.d.mts +1019 -374
- package/dist/index.d.ts +1019 -374
- package/dist/index.mjs +1 -19
- package/dist/packem_chunks/dispatch.mjs +1 -0
- package/dist/packem_chunks/handler.mjs +1 -160
- package/dist/packem_chunks/handler10.mjs +1 -16
- package/dist/packem_chunks/handler11.mjs +1 -22
- package/dist/packem_chunks/handler12.mjs +1 -192
- package/dist/packem_chunks/handler13.mjs +3 -131
- package/dist/packem_chunks/handler14.mjs +1 -65
- package/dist/packem_chunks/handler15.mjs +1 -58
- package/dist/packem_chunks/handler16.mjs +1 -80
- package/dist/packem_chunks/handler17.mjs +7 -43
- package/dist/packem_chunks/handler18.mjs +1 -105
- package/dist/packem_chunks/handler19.mjs +1 -170
- package/dist/packem_chunks/handler2.mjs +1 -114
- package/dist/packem_chunks/handler20.mjs +3 -94
- package/dist/packem_chunks/handler21.mjs +3 -94
- package/dist/packem_chunks/handler22.mjs +1 -0
- package/dist/packem_chunks/handler23.mjs +2 -0
- package/dist/packem_chunks/handler24.mjs +2 -0
- package/dist/packem_chunks/handler25.mjs +99 -0
- package/dist/packem_chunks/handler26.mjs +9 -0
- package/dist/packem_chunks/handler27.mjs +1 -0
- package/dist/packem_chunks/handler3.mjs +1 -204
- package/dist/packem_chunks/handler4.mjs +3 -33
- package/dist/packem_chunks/handler5.mjs +1 -49
- package/dist/packem_chunks/handler6.mjs +1 -91
- package/dist/packem_chunks/handler7.mjs +1 -42
- package/dist/packem_chunks/handler8.mjs +2 -174
- package/dist/packem_chunks/handler9.mjs +1 -315
- package/dist/packem_chunks/runCodegenCommand.mjs +4 -52
- package/dist/packem_chunks/runDeployCommand.mjs +11 -594
- package/dist/packem_chunks/runDevCommand.mjs +2 -0
- package/dist/packem_chunks/runInitCommand.mjs +164 -1432
- package/dist/packem_chunks/runResetCommand.mjs +1 -41
- package/dist/packem_chunks/runRpcCommand.mjs +1 -68
- package/dist/packem_shared/COMMANDS-JClcR9JT.mjs +1 -0
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-CyIsI3S1.mjs +1 -0
- package/dist/packem_shared/EXIT_CODE-D_XCixv8.mjs +1 -0
- package/dist/packem_shared/admin-token-VdUnvnKW.mjs +1 -0
- package/dist/packem_shared/admin-url-DJB8P6Jh.mjs +1 -0
- package/dist/packem_shared/binding-manifest-file-CQ1eQYy1.mjs +2 -0
- package/dist/packem_shared/buildRegistryIndex-Dc8D8AR6.mjs +1 -0
- package/dist/packem_shared/cap-error-body-YBKO32BF.mjs +1 -0
- package/dist/packem_shared/catalog-CahzmDLV.mjs +1 -0
- package/dist/packem_shared/cli-DwEpuZm9.mjs +2 -0
- package/dist/packem_shared/cli-manifest-CRet0egA.mjs +1 -0
- package/dist/packem_shared/cloudflare-credentials-BVmZnDZk.mjs +1 -0
- package/dist/packem_shared/codegen-error-AmH54ofi.mjs +3 -0
- package/dist/packem_shared/command-CAXl2A1S.mjs +1 -0
- package/dist/packem_shared/commands-DgUDxkA8.mjs +13 -0
- package/dist/packem_shared/createLogger-Cl18I8AX.mjs +2 -0
- package/dist/packem_shared/createRecordingSpawner-Ch7zuQQE.mjs +1 -0
- package/dist/packem_shared/deploy-target-DCWwiuAe.mjs +1 -0
- package/dist/packem_shared/diffSnapshots-htqNZT8Q.mjs +7 -0
- package/dist/packem_shared/docker-AqLAkOyY.mjs +1 -0
- package/dist/packem_shared/health-probe-Bw9JFfIy.mjs +1 -0
- package/dist/packem_shared/import-BRM1hs7j.mjs +12 -0
- package/dist/packem_shared/index-oHZBGBwB.mjs +1 -0
- package/dist/packem_shared/insertSchemaExtension-DuiV6cba.mjs +8 -0
- package/dist/packem_shared/lint-ignore-report-DKZagpqk.mjs +2 -0
- package/dist/packem_shared/mcp-config-file-CcDTeXIq.mjs +2 -0
- package/dist/packem_shared/open-url-EnKy--w-.mjs +1 -0
- package/dist/packem_shared/output-format-B4eQHi5y.mjs +2 -0
- package/dist/packem_shared/parseManifest-CwPTKdtS.mjs +1 -0
- package/dist/packem_shared/path-containment-CgxYZggb.mjs +1 -0
- package/dist/packem_shared/plan-Dl57T11P.mjs +5 -0
- package/dist/packem_shared/planDevCommand-D8sm56hO.mjs +1 -0
- package/dist/packem_shared/platform-diagnostics-Cn2g6Jh-.mjs +4 -0
- package/dist/packem_shared/prompt-cancelled-BvsNxg_Q.mjs +1 -0
- package/dist/packem_shared/render-lunora-error-C3OJY5wk.mjs +2 -0
- package/dist/packem_shared/resolve-BrsV4Qyc.mjs +5 -0
- package/dist/packem_shared/resolve-target-C_ZloTvd.mjs +1 -0
- package/dist/packem_shared/runAddCommand-DTvfp4h2.mjs +1 -0
- package/dist/packem_shared/runExportCommand-slAq5BCF.mjs +5 -0
- package/dist/packem_shared/runMigrateGenerateCommand-BwemPIOz.mjs +11 -0
- package/dist/packem_shared/schemaIrToSnapshot-BHuNOfgs.mjs +1 -0
- package/dist/packem_shared/secret-key-6bCftsxj.mjs +1 -0
- package/dist/packem_shared/shared-D-zCOmgY.mjs +1 -0
- package/dist/packem_shared/storage-DWhTX8eS.mjs +1 -0
- package/dist/packem_shared/tui-prompts-B3YwUhGw.mjs +4 -0
- package/dist/packem_shared/variables-CPYyJtsL.mjs +1 -0
- package/dist/packem_shared/wrangler-name-Dsk5K1f-.mjs +1 -0
- package/dist/packem_shared/wrangler-secrets-3chIVv3H.mjs +1 -0
- package/package.json +39 -19
- package/skills/README.md +35 -17
- package/skills/lunora/SKILL.md +154 -64
- package/skills/lunora-create-package/SKILL.md +97 -84
- package/skills/lunora-deploy/SKILL.md +175 -105
- package/skills/lunora-functions/SKILL.md +362 -56
- package/skills/lunora-migration-helper/SKILL.md +162 -137
- package/skills/lunora-performance-audit/SKILL.md +137 -95
- package/skills/lunora-quickstart/SKILL.md +137 -136
- package/skills/lunora-realtime/SKILL.md +140 -99
- package/skills/lunora-setup-auth/SKILL.md +162 -102
- package/skills/lunora-setup-hyperdrive/SKILL.md +114 -97
- package/skills/lunora-setup-hyperdrive-global/SKILL.md +93 -90
- package/skills/lunora-setup-mail/SKILL.md +85 -113
- package/skills/lunora-setup-scheduler/SKILL.md +132 -96
- package/skills/lunora-setup-storage/SKILL.md +189 -95
- package/dist/packem_chunks/planDevCommand.mjs +0 -541
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +0 -397
- package/dist/packem_shared/COMMANDS-B0ftFD_3.mjs +0 -948
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +0 -244
- package/dist/packem_shared/admin-url-4UzT-CI4.mjs +0 -19
- package/dist/packem_shared/api-spec-CtA6ilu4.mjs +0 -13
- package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +0 -38
- package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +0 -31
- package/dist/packem_shared/command-D3lB_4Az.mjs +0 -19
- package/dist/packem_shared/commands-B-gR09Z_.mjs +0 -845
- package/dist/packem_shared/createLogger-B40gPzQo.mjs +0 -78
- package/dist/packem_shared/createRecordingSpawner-DxI3mebw.mjs +0 -43
- package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +0 -61
- package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +0 -161
- package/dist/packem_shared/docker-hMQ97KSQ.mjs +0 -21
- package/dist/packem_shared/insertSchemaExtension-DAqbfr9Z.mjs +0 -64
- package/dist/packem_shared/open-url-Dfq6fAyT.mjs +0 -41
- package/dist/packem_shared/output-format-wUvAN6AL.mjs +0 -17
- package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
- package/dist/packem_shared/parseManifest--vZf2FY1.mjs +0 -94
- package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +0 -9
- package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +0 -16
- package/dist/packem_shared/runAddCommand-bnY6-HKb.mjs +0 -4
- package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +0 -79
- package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +0 -43
- package/dist/packem_shared/storage-BIsph-Vk.mjs +0 -84
- package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +0 -658
- package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +0 -12
- package/dist/packem_shared/wrangler-secrets-P2_ZUR-k.mjs +0 -47
|
@@ -1,110 +1,153 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lunora-setup-auth
|
|
3
|
-
description: Adds authentication to a Lunora app.
|
|
4
|
-
OAuth (Clerk, Auth0), magic link, or email OTP via `lunora registry add auth`,
|
|
5
|
-
wiring the auth handler into the Worker, and gating functions on the session.
|
|
3
|
+
description: Adds authentication to a Lunora app with the `auth` registry item (better-auth via `@lunora/auth`, users and sessions in D1). Covers email/password sign-up and sign-in, Clerk or Auth0 OAuth, magic link, email OTP, copy-in auth screens (`lunora add auth-ui`), wiring `/api/auth/*` and `resolveIdentity` into the Worker, and gating functions on `ctx.auth.userId`. Use when the user asks to "add auth", "add login/sign-in/sign-up", "protect this query", or "add Clerk/Auth0/magic link/OTP", runs `lunora add auth` or `lunora registry add auth`, edits `lunora/auth/index.ts`, or `ctx.auth.userId` is unexpectedly `null`.
|
|
6
4
|
---
|
|
7
5
|
|
|
8
6
|
# Lunora Setup Auth
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
The `auth` registry item is built on `@lunora/auth`, a thin wrapper over
|
|
9
|
+
[better-auth](https://www.better-auth.com). Users and sessions are stored in D1.
|
|
10
|
+
Lunora's `SessionDO` is a separate TTL'd token store that `@lunora/auth` never
|
|
11
|
+
calls, so don't wire it for auth. If you need auth state in a Durable Object
|
|
12
|
+
instead of D1, use `LunoraAuthDO` / `.auth({ namespace, internalSecret })`.
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
If the project has no Lunora backend yet, start with `lunora-quickstart`.
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
- Adding an OAuth/OIDC provider (Clerk, Auth0), magic link, or email OTP.
|
|
19
|
-
- Gating queries/mutations on the signed-in user.
|
|
16
|
+
## Step 1: Add the item
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
```bash
|
|
19
|
+
lunora add auth # asks for the provider and D1 database name
|
|
20
|
+
lunora add auth --provider clerk --db my-app-db # non-interactive (auth | clerk | auth0)
|
|
21
|
+
lunora registry add auth # low-level: writes placeholder names
|
|
22
|
+
```
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
- You only need to read `ctx.auth.userId` in a function and auth is already
|
|
25
|
-
installed — just use it.
|
|
24
|
+
Then run `pnpm install`. The item does the following:
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
1. Adds `@lunora/auth`, `@lunora/mail` and `@lunora/server` to `package.json`.
|
|
27
|
+
2. Copies `lunora/auth/index.ts` into the project. The project owns this file and can edit it. It exports `buildAuth(env)`, `getAuth(env)` (memoized per isolate) and `mountAuth(env, request)`, which routes `/api/auth/*` and returns `undefined` for any other path. The scaffold turns on email/password with `requireEmailVerification: true` and sends verification and reset mail through `@lunora/mail`. It also adds the `uiConfig()` plugin.
|
|
28
|
+
3. Adds a D1 `DB` binding to `wrangler.jsonc`. The `database_id` is a placeholder.
|
|
29
|
+
4. Writes `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL` and `MAIL_FROM` to `.dev.vars`.
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
2. Mount the auth request handler in the Worker entry.
|
|
31
|
-
3. Configure env vars and the D1 database.
|
|
32
|
-
4. (Optional) Layer a provider item (Clerk / Auth0 / magic link / OTP) on top.
|
|
33
|
-
5. Gate functions on `ctx.auth.userId`; gate UI with the auth gates/hooks.
|
|
31
|
+
## Step 2: Database and env vars
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
| Var | Secret | Notes |
|
|
34
|
+
| -------------------- | ------ | --------------------------------------------------------------------------------- |
|
|
35
|
+
| `BETTER_AUTH_SECRET` | yes | At least 32 chars (`openssl rand -base64 32`). Production: `wrangler secret put`. |
|
|
36
|
+
| `BETTER_AUTH_URL` | no | Public base URL. better-auth uses it for callbacks and cookies. |
|
|
37
|
+
| `MAIL_FROM` | no | Sender for verification and reset mail. |
|
|
36
38
|
|
|
37
39
|
```bash
|
|
38
|
-
|
|
40
|
+
wrangler d1 create my-app-db # paste the returned id into the DB binding
|
|
41
|
+
lunora doctor # flags a placeholder database_id
|
|
39
42
|
```
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
Don't declare the better-auth tables (`user`, `session`, `account`,
|
|
45
|
+
`verification`) in `lunora/schema.ts`. better-auth owns them in D1. In dev the
|
|
46
|
+
schema is auto-applied on the first `/api/auth/*` request. For production,
|
|
47
|
+
pre-apply it with `compileMigrationsSql(auth.options)` piped to
|
|
48
|
+
`wrangler d1 execute <db> --remote --file -`. The item README has the script.
|
|
49
|
+
Re-run that migration after adding a plugin that brings its own tables, such as
|
|
50
|
+
`organization` or `twoFactor`.
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
(run `pnpm install` afterwards).
|
|
45
|
-
2. Copies `lunora/auth/index.ts` — the auth instance (`buildAuth` / `getAuth`)
|
|
46
|
-
and the `/api/auth/*` request handler (`mountAuth`) — into your project. It is
|
|
47
|
-
**yours** to edit.
|
|
48
|
-
3. Adds a D1 `DB` binding to `wrangler.jsonc` (better-auth persists
|
|
49
|
-
users/sessions there).
|
|
50
|
-
4. Scaffolds `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL`, and `MAIL_FROM` into
|
|
51
|
-
`.dev.vars`.
|
|
52
|
+
## Step 3: Wire the Worker
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
There are two parts. `/api/auth/*` has to reach better-auth. `resolveIdentity`
|
|
55
|
+
has to turn the session cookie into `ctx.auth`. If `resolveIdentity` is missing,
|
|
56
|
+
sign-in still works but `ctx.auth.userId` stays `null` in every function.
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
**Through the generated `defineApp` builder.** This is the default for
|
|
59
|
+
templates. `.auth()` does both parts: it builds the instance lazily, runs
|
|
60
|
+
`ensureMigrated`, dispatches `/api/auth/*` and sets `resolveIdentity`. A
|
|
61
|
+
Vite-first app (`"main": "virtual:lunora/worker"`) has no entry file, so chain
|
|
62
|
+
the call in the `lunora.config.ts` `app` hook. A hand-written entry
|
|
63
|
+
(`src/server.ts`) chains it on `defineApp()` directly:
|
|
58
64
|
|
|
59
|
-
|
|
65
|
+
```ts
|
|
66
|
+
// lunora.config.ts
|
|
67
|
+
import type { AppBuilder, LunoraConfig } from "./lunora/_generated/app";
|
|
68
|
+
// Move the object `buildAuth` passes to `createAuth` (minus `database`; the
|
|
69
|
+
// builder supplies the D1 adapter) into an exported `authOptions(env)` in
|
|
70
|
+
// lunora/auth/index.ts, and have `buildAuth` spread it too.
|
|
71
|
+
import { authOptions } from "./lunora/auth/index.js";
|
|
72
|
+
|
|
73
|
+
interface Env {
|
|
74
|
+
BETTER_AUTH_SECRET: string;
|
|
75
|
+
BETTER_AUTH_URL: string;
|
|
76
|
+
DB: D1Database;
|
|
77
|
+
}
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
export default {
|
|
80
|
+
target: "cloudflare",
|
|
81
|
+
app: (app: AppBuilder<Env>): AppBuilder<Env> =>
|
|
82
|
+
app.auth({
|
|
83
|
+
d1: (env) => env.DB,
|
|
84
|
+
options: (env) => authOptions(env),
|
|
85
|
+
}),
|
|
86
|
+
} satisfies LunoraConfig<Env>;
|
|
87
|
+
```
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
`
|
|
89
|
+
Reuse the scaffold's options rather than retyping a subset: a hand-written
|
|
90
|
+
`{ emailAndPassword: { enabled: true } }` drops `requireEmailVerification`,
|
|
91
|
+
`sendResetPassword` and `sendVerificationEmail`, so users sign in unverified and
|
|
92
|
+
no verification or reset mail goes out.
|
|
69
93
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
```
|
|
94
|
+
`.auth()` appears on the builder only after codegen sees `@lunora/auth`
|
|
95
|
+
installed, so run `lunora codegen` first.
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`compileMigrationsSql(auth.options)` piped to `wrangler d1 execute`. Run
|
|
78
|
-
`lunora doctor` to confirm the `DB` binding has a real `database_id` (not a
|
|
79
|
-
placeholder).
|
|
97
|
+
**Hand-rolled `createWorker` entry.** Call the scaffolded `mountAuth` first in
|
|
98
|
+
`fetch`, then hand every other request to the Lunora worker, which resolves the
|
|
99
|
+
identity:
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
```ts
|
|
102
|
+
import type { AuthEnv } from "../../lunora/auth/index.js";
|
|
103
|
+
import { getAuth, mountAuth } from "../../lunora/auth/index.js";
|
|
104
|
+
|
|
105
|
+
const worker = createWorker({
|
|
106
|
+
resolveIdentity: async (req, env) => {
|
|
107
|
+
const session = await getAuth(env as AuthEnv).api.getSession({ headers: req.headers });
|
|
108
|
+
return session?.user ? { userId: session.user.id } : null;
|
|
109
|
+
},
|
|
110
|
+
// …
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export default {
|
|
114
|
+
async fetch(request: Request, env: AuthEnv, ctx: ExecutionContext): Promise<Response> {
|
|
115
|
+
const authResponse = await mountAuth(env, request);
|
|
116
|
+
if (authResponse) return authResponse;
|
|
117
|
+
return worker.fetch(request, env, ctx); // RPC, WebSocket, everything else
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
```
|
|
84
121
|
|
|
85
|
-
## Step 4:
|
|
122
|
+
## Step 4: Providers and add-ons (optional)
|
|
86
123
|
|
|
87
|
-
Each
|
|
124
|
+
Each of these items requires the base `auth` item and installs it if it is
|
|
125
|
+
missing:
|
|
88
126
|
|
|
89
127
|
```bash
|
|
90
|
-
lunora
|
|
91
|
-
lunora
|
|
92
|
-
lunora
|
|
93
|
-
lunora
|
|
128
|
+
lunora add auth-clerk # Clerk OIDC (genericOAuth): CLERK_CLIENT_ID / _SECRET / CLERK_ISSUER_URL
|
|
129
|
+
lunora add auth-auth0 # Auth0 OIDC: AUTH0_CLIENT_ID / _SECRET / AUTH0_DOMAIN
|
|
130
|
+
lunora add auth-magic-link # passwordless link, sent via @lunora/mail
|
|
131
|
+
lunora add auth-otp # email one-time code, sent via @lunora/mail
|
|
132
|
+
lunora add auth-emails # styled React templates for the auth mails (renderEmail)
|
|
133
|
+
lunora add auth-ui # copy-in screens; framework auto-detected (no React Native port)
|
|
94
134
|
```
|
|
95
135
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
`.
|
|
136
|
+
The provider items copy a file into `lunora/auth/` but don't register it
|
|
137
|
+
anywhere. Add the plugin to the `plugins` array yourself, in `buildAuth` or in
|
|
138
|
+
`.auth({ options })`. For example, `plugins: [uiConfig(), clerk(env)]` with
|
|
139
|
+
`import { clerk } from "./clerk.js"`. The other exports are `auth0`,
|
|
140
|
+
`magicLinkPlugin` and `emailOtpPlugin`. The OAuth callback URL is
|
|
141
|
+
`<BETTER_AUTH_URL>/api/auth/oauth2/callback/<clerk|auth0>`.
|
|
99
142
|
|
|
100
143
|
## Step 5: Use the session
|
|
101
144
|
|
|
102
145
|
### In functions
|
|
103
146
|
|
|
104
|
-
The runtime resolves the session and exposes the user on every context:
|
|
105
|
-
|
|
106
147
|
```ts
|
|
107
|
-
import { LunoraError
|
|
148
|
+
import { LunoraError } from "lunorash/server";
|
|
149
|
+
|
|
150
|
+
import { mutation, v } from "#lunora/_generated/server.js";
|
|
108
151
|
|
|
109
152
|
export const createDocument = mutation.input({ title: v.string() }).mutation(async ({ ctx, args: { title } }) => {
|
|
110
153
|
if (!ctx.auth.userId) {
|
|
@@ -114,27 +157,41 @@ export const createDocument = mutation.input({ title: v.string() }).mutation(asy
|
|
|
114
157
|
});
|
|
115
158
|
```
|
|
116
159
|
|
|
117
|
-
|
|
118
|
-
|
|
160
|
+
`ctx.auth.getIdentity()` returns the full session claims, such as email and
|
|
161
|
+
name. For org or role checks, compose `withAuthPlugins(auth)` and call the
|
|
162
|
+
better-auth server API.
|
|
163
|
+
|
|
164
|
+
### In the client
|
|
119
165
|
|
|
120
|
-
|
|
166
|
+
The default is a better-auth **cookie session**. `LunoraClient` needs no token,
|
|
167
|
+
so don't call `setToken` / `setAuthToken` for it. Sign in with the better-auth
|
|
168
|
+
client. Build that client with `createLunoraAuthClient`, which installs
|
|
169
|
+
`lunoraSessionSync()`. That plugin tells every open `LunoraClient`, including
|
|
170
|
+
ones in other tabs, to re-resolve the session after sign-in or sign-out. Without
|
|
171
|
+
it, live queries keep serving the previous user's rows. If you call
|
|
172
|
+
`createAuthClient` yourself, add `lunoraSessionSync()` to its plugins.
|
|
121
173
|
|
|
122
174
|
```tsx
|
|
123
|
-
import {
|
|
175
|
+
import { createLunoraAuthClient } from "@lunora/auth/plugins/client";
|
|
176
|
+
import { Authenticated, AuthLoading, Unauthenticated, useAuth } from "@lunora/react";
|
|
177
|
+
import { createAuthClient } from "better-auth/react";
|
|
124
178
|
|
|
125
|
-
|
|
126
|
-
|
|
179
|
+
export const authClient = createLunoraAuthClient(createAuthClient);
|
|
180
|
+
|
|
181
|
+
function Account({ email, password }: { email: string; password: string }) {
|
|
182
|
+
const { user } = useAuth(); // { setToken, status, token, user }
|
|
127
183
|
|
|
128
184
|
return (
|
|
129
185
|
<>
|
|
186
|
+
<AuthLoading>…</AuthLoading>
|
|
130
187
|
<Authenticated>
|
|
131
188
|
<span>Signed in as {user?.email}</span>
|
|
132
|
-
<button type="button" onClick={() => signOut()}>
|
|
189
|
+
<button type="button" onClick={() => authClient.signOut()}>
|
|
133
190
|
Sign out
|
|
134
191
|
</button>
|
|
135
192
|
</Authenticated>
|
|
136
193
|
<Unauthenticated>
|
|
137
|
-
<button type="button" onClick={() => signIn()}>
|
|
194
|
+
<button type="button" onClick={() => authClient.signIn.email({ email, password })}>
|
|
138
195
|
Sign in
|
|
139
196
|
</button>
|
|
140
197
|
</Unauthenticated>
|
|
@@ -143,28 +200,31 @@ function Account() {
|
|
|
143
200
|
}
|
|
144
201
|
```
|
|
145
202
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
-
|
|
166
|
-
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
203
|
+
A client plugin toggle such as `{ plugins: { twoFactor: true } }` only adds
|
|
204
|
+
the client half. Register the matching server plugin (`twoFactor()` from
|
|
205
|
+
`@lunora/auth/plugins`) in `buildAuth`'s `plugins` too, or its endpoints don't
|
|
206
|
+
exist.
|
|
207
|
+
|
|
208
|
+
`setToken` is only for bearer-token setups, such as React Native or a JWT from
|
|
209
|
+
an external IdP.
|
|
210
|
+
|
|
211
|
+
Branch on `status`, not on `user === null`. `status` takes one of four values:
|
|
212
|
+
`"unauthenticated"`, `"loading"`, `"authenticated"` or `"unreachable"`. The gates
|
|
213
|
+
in `@lunora/vue`, `@lunora/solid`, `@lunora/svelte` and `@lunora/angular` use the
|
|
214
|
+
same four. `"unreachable"` means a credential is held but the session endpoint
|
|
215
|
+
failed, for example on an offline reload. The gates treat it as authenticated,
|
|
216
|
+
yet `user` can still be `null` at that point. A UI that reads that `null` as
|
|
217
|
+
"signed out" shows a signed-in user the signed-out screen. The full contract is
|
|
218
|
+
documented on `AuthStatus` in `@lunora/client/auth`.
|
|
219
|
+
|
|
220
|
+
## Pitfalls
|
|
221
|
+
|
|
222
|
+
- **Sign-in rejected right after sign-up.** The scaffold sets `requireEmailVerification: true`, so the user has to open the verification link first. In dev, that link is in the Studio Mail tab.
|
|
223
|
+
- **Mail in production.** Dev captures mail into the Studio. In production with no `MAIL_FROM`, the scaffold throws rather than logging the link. Auth links are bearer credentials, so this is intentional. Production needs `lunora add email` (the `SEND_EMAIL` binding) or `RESEND_API_KEY`, plus a verified sender domain. See `lunora-setup-mail`.
|
|
224
|
+
- **Unauthenticated storage or other items.** Items that call `requireOwner` throw `UNAUTHORIZED` until `resolveIdentity` is wired (Step 3).
|
|
225
|
+
|
|
226
|
+
## Verify
|
|
227
|
+
|
|
228
|
+
1. Run `lunora doctor`. It should report no placeholder `database_id`.
|
|
229
|
+
2. Run `lunora dev`. Sign up, open the verification link from the Studio Mail tab, then sign in.
|
|
230
|
+
3. Call a query that returns `ctx.auth.userId`. It should return the user's id, not `null`.
|
|
@@ -1,73 +1,42 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lunora-setup-hyperdrive
|
|
3
|
-
description: Connects an existing Postgres/MySQL database to a Lunora app
|
|
3
|
+
description: Connects an existing, externally owned Postgres/MySQL database to a Lunora app through Cloudflare Hyperdrive. Lunora does not own its schema. Covers `@lunora/hyperdrive`, the action-only, non-reactive `ctx.sql` client (`createHyperdrive` + `fromPostgresJs`/`fromNodePg`/`fromMysql2`, wired with the app builder's `.hyperdrive()`), the `HYPERDRIVE` binding and `wrangler hyperdrive create`, and making external rows live through a projection or a `.source()` ingest table. Use when the user wants to "query our existing Postgres", "read the legacy DB from an action", "connect Neon/RDS/Supabase", or sync external rows into Lunora, or sees `ctx.sql` / `hyperdrive_outside_action` errors. For storing Lunora's own `.global()` tables in Postgres/MySQL with reactive live queries, use `lunora-setup-hyperdrive-global` instead.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Lunora Setup Hyperdrive
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
17
|
-
>
|
|
18
|
-
>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## When to Use
|
|
23
|
-
|
|
24
|
-
- Reading/writing an existing Postgres or MySQL database from a Lunora action.
|
|
25
|
-
- Incrementally migrating off a legacy DB while standing up Lunora alongside it.
|
|
26
|
-
- Backfilling/syncing external rows into reactive Lunora tables (projection).
|
|
27
|
-
|
|
28
|
-
## When Not to Use
|
|
29
|
-
|
|
30
|
-
- You want the external data to be **reactive** out of the box — it can't be;
|
|
31
|
-
Hyperdrive writes don't hit the change-feed. Project into a `defineSchema`
|
|
32
|
-
table instead.
|
|
33
|
-
- You're reaching for `ctx.sql` inside a `query` or `mutation` — forbidden by
|
|
34
|
-
design (the advisor lint flags it). Move the access into an `action`.
|
|
35
|
-
- The app has no Lunora backend yet — use `lunora-quickstart` first.
|
|
36
|
-
- You'd be replacing `defineSchema` entirely — then you lose realtime, OCC,
|
|
37
|
-
optimistic updates, the offline queue, and the advisors. Use Lunora's own data
|
|
38
|
-
layer for app state.
|
|
39
|
-
|
|
40
|
-
## Workflow
|
|
41
|
-
|
|
42
|
-
1. Install `@lunora/hyperdrive` and one SQL driver.
|
|
43
|
-
2. Create the Hyperdrive config (`wrangler hyperdrive create`) and add the
|
|
44
|
-
`HYPERDRIVE` binding to `wrangler.jsonc`.
|
|
45
|
-
3. Regenerate types with `lunora codegen` so `ctx.sql` appears on `ActionCtx`.
|
|
46
|
-
4. Read/write the external DB from an action via `ctx.sql`.
|
|
47
|
-
5. (Optional) Project external rows into a `defineSchema` table to make them
|
|
48
|
-
reactive.
|
|
49
|
-
|
|
50
|
-
## Step 1: Install the package + a driver
|
|
8
|
+
`@lunora/hyperdrive` exposes a Cloudflare Hyperdrive binding as a
|
|
9
|
+
driver-agnostic `ctx.sql` client that is available only inside an `action`. It integrates
|
|
10
|
+
a database Lunora doesn't own. Don't use it to replace `defineSchema`, because you'd lose
|
|
11
|
+
realtime, OCC, optimistic updates, the offline queue and the advisors.
|
|
12
|
+
|
|
13
|
+
> **`ctx.sql` is not reactive.** Lunora cannot see this database, so subscriptions
|
|
14
|
+
> do not re-run when its rows change, including rows you write through `ctx.sql`.
|
|
15
|
+
> Queries are non-deterministic, so `ctx.sql` exists only on `ActionCtx`.
|
|
16
|
+
> The `hyperdrive_outside_action` advisor lint flags any use in a
|
|
17
|
+
> `query`/`mutation`. To make the data live, see Step 5. If you instead want Lunora-owned tables
|
|
18
|
+
> stored in Postgres/MySQL and fully reactive, use `lunora-setup-hyperdrive-global`.
|
|
19
|
+
|
|
20
|
+
## Step 1: Install the package + one driver
|
|
51
21
|
|
|
52
22
|
```bash
|
|
53
23
|
pnpm add @lunora/hyperdrive
|
|
54
|
-
|
|
55
|
-
pnpm add
|
|
56
|
-
# or
|
|
57
|
-
pnpm add pg # node-postgres → fromNodePg ($1, $2 placeholders)
|
|
58
|
-
# or
|
|
59
|
-
pnpm add mysql2 # mysql2 → fromMysql2 (? placeholders)
|
|
24
|
+
pnpm add postgres # → fromPostgresJs ($1, $2 placeholders)
|
|
25
|
+
# or: pnpm add pg → fromNodePg ($1, $2)
|
|
26
|
+
# or: pnpm add mysql2 → fromMysql2 (?)
|
|
60
27
|
```
|
|
61
28
|
|
|
29
|
+
The drivers are optional peer dependencies, so none ships with the package.
|
|
30
|
+
The package never rewrites SQL, so use your driver's native placeholders.
|
|
31
|
+
|
|
62
32
|
## Step 2: Create the binding
|
|
63
33
|
|
|
64
34
|
```bash
|
|
65
35
|
wrangler hyperdrive create my-db --connection-string="postgres://user:pass@host:5432/db" # gitleaks:allow -- placeholder, not a real secret
|
|
66
36
|
```
|
|
67
37
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
proxy locally):
|
|
38
|
+
Add the printed `id` to `wrangler.jsonc`. `localConnectionString` lets `lunora dev`
|
|
39
|
+
connect directly:
|
|
71
40
|
|
|
72
41
|
```jsonc
|
|
73
42
|
{
|
|
@@ -81,10 +50,9 @@ proxy locally):
|
|
|
81
50
|
}
|
|
82
51
|
```
|
|
83
52
|
|
|
84
|
-
Lunora validates
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`@lunora/hyperdrive` surfaces a hint reminding you to add it.
|
|
53
|
+
Lunora validates the entry but never writes the `id` itself, because only
|
|
54
|
+
`wrangler hyperdrive create` can mint it. Validation fails without a `binding`
|
|
55
|
+
and warns when `id` is empty.
|
|
88
56
|
|
|
89
57
|
## Step 3: Regenerate types
|
|
90
58
|
|
|
@@ -92,63 +60,112 @@ that's a remote resource only `wrangler hyperdrive create` can mint. Importing
|
|
|
92
60
|
lunora codegen
|
|
93
61
|
```
|
|
94
62
|
|
|
95
|
-
When
|
|
96
|
-
|
|
97
|
-
determinism/realtime caveat.
|
|
63
|
+
When `ctx.sql` is used, codegen adds `readonly sql: SqlClient` to `ActionCtx` only. It also
|
|
64
|
+
emits a `.hyperdrive()` method on the generated app builder.
|
|
98
65
|
|
|
99
|
-
## Step 4:
|
|
66
|
+
## Step 4: Wire the client on the app builder
|
|
100
67
|
|
|
101
|
-
|
|
68
|
+
`ctx.sql` is `readonly`, so assigning it in a handler is a `TS2540` error.
|
|
69
|
+
Codegen can't build the client because it depends on your driver, so supply a
|
|
70
|
+
thunk instead. It runs once per shard construction, not per request:
|
|
71
|
+
|
|
72
|
+
```ts title="src/server.ts"
|
|
73
|
+
import type { HyperdriveLike } from "@lunora/hyperdrive";
|
|
102
74
|
import { createHyperdrive, fromPostgresJs } from "@lunora/hyperdrive";
|
|
103
75
|
import postgres from "postgres";
|
|
104
76
|
|
|
105
|
-
import {
|
|
77
|
+
import { defineApp } from "../lunora/_generated/app.js";
|
|
106
78
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
79
|
+
const app = defineApp<Env>()
|
|
80
|
+
.shard((env) => env.SHARD)
|
|
81
|
+
.hyperdrive((env) => fromPostgresJs(postgres(createHyperdrive(env.HYPERDRIVE as HyperdriveLike).connectionString)))
|
|
82
|
+
.build();
|
|
83
|
+
|
|
84
|
+
export const ShardDO = app.ShardDO;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Then query from any action:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { LunoraError } from "lunorash/server";
|
|
110
91
|
|
|
111
|
-
|
|
92
|
+
import { action, v } from "#lunora/_generated/server.js";
|
|
93
|
+
|
|
94
|
+
export const listLegacyOrders = action.input({ orgId: v.string() }).action(async ({ ctx, args: { orgId } }) => {
|
|
95
|
+
if (!ctx.auth.userId) {
|
|
96
|
+
throw new LunoraError("UNAUTHORIZED", "not signed in");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// `orgId` comes from the client: the join scopes it to orgs the caller belongs to.
|
|
100
|
+
return ctx.sql.query<{ id: string; total: number }>(
|
|
101
|
+
"select o.id, o.total from orders o join org_members m on m.org_id = o.org where o.org = $1 and m.user_id = $2",
|
|
102
|
+
[orgId, ctx.auth.userId],
|
|
103
|
+
);
|
|
112
104
|
});
|
|
113
105
|
```
|
|
114
106
|
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
**Verify:** call the action from the studio's function runner and check that it returns rows.
|
|
108
|
+
|
|
109
|
+
## Step 5 (optional): Make external data reactive
|
|
117
110
|
|
|
118
|
-
|
|
111
|
+
Pick one approach:
|
|
119
112
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
`defineSchema` table from the same action — that write _is_ tracked:
|
|
113
|
+
**A. Projection from an action.** This works when your app is the writer. Write the row into a
|
|
114
|
+
`defineSchema` table through a mutation. That write is tracked:
|
|
123
115
|
|
|
124
116
|
```ts
|
|
117
|
+
import { LunoraError } from "lunorash/server";
|
|
118
|
+
|
|
119
|
+
import { internal } from "#lunora/_generated/internal.js";
|
|
120
|
+
|
|
125
121
|
const [row] = await ctx.sql.query<{ id: string; total: number }>("select id, total from orders where id = $1", [id]);
|
|
126
122
|
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
if (!row) {
|
|
124
|
+
throw new LunoraError("NOT_FOUND", `order ${id} not found`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Re-runs live queries over `orders`. ctx.run* takes a generated reference, not a "file:fn" string.
|
|
128
|
+
await ctx.runMutation(internal.orders.upsert, { id: row.id, total: row.total });
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
+
**B. A `.source()` table.** This works when other systems also write the database. Lunora polls
|
|
132
|
+
the external query on the shard's alarm, diffs the result, materializes it into the
|
|
133
|
+
DO's SQLite and notifies subscribers, so you don't write an action or a cron:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// lunora/schema.ts
|
|
137
|
+
messages: defineTable({ body: v.string(), channelId: v.string() })
|
|
138
|
+
.shardBy("channelId")
|
|
139
|
+
.source({
|
|
140
|
+
binding: "HYPERDRIVE",
|
|
141
|
+
query: 'select id, body, channel_id as "channelId" from messages where channel_id = $1',
|
|
142
|
+
tenantBy: (shardKey) => [shardKey], // required under .shardBy(): the tenant boundary
|
|
143
|
+
}),
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// worker entry: one client per binding name
|
|
148
|
+
.sourceClient((env, binding) => fromPostgresJs(postgres((env[binding] as { connectionString: string }).connectionString)))
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
How `.source()` behaves:
|
|
152
|
+
|
|
153
|
+
- Freshness depends on the poll interval: every alarm tick by default,
|
|
154
|
+
`refresh: { everyMs }` to throttle it, or `"manual"`.
|
|
155
|
+
- The table is read-only from Lunora: rows come only from the ingest loop.
|
|
156
|
+
- The default `mode: "full-pull"` re-reads the whole slice on every tick, which
|
|
157
|
+
works up to about 10k rows. For larger slices, use `mode: "incremental"` with a
|
|
158
|
+
`cursor`, plus either `reconcileEveryMs` or `softDeleteColumn` so deletes are
|
|
159
|
+
still detected.
|
|
160
|
+
- A source table can't also be `.global()`.
|
|
161
|
+
|
|
131
162
|
## Common Pitfalls
|
|
132
163
|
|
|
133
|
-
1. **`ctx.sql` in a query/mutation.** It's action-only
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
install the one you use; don't assume one ships with the package.
|
|
142
|
-
5. **Wrong placeholder syntax.** `$1, $2` for Postgres drivers, `?` for mysql2.
|
|
143
|
-
The package does not rewrite SQL.
|
|
144
|
-
|
|
145
|
-
## Checklist
|
|
146
|
-
|
|
147
|
-
- [ ] `@lunora/hyperdrive` + one driver (`postgres`/`pg`/`mysql2`) installed.
|
|
148
|
-
- [ ] `wrangler hyperdrive create` run; `HYPERDRIVE` binding added to
|
|
149
|
-
`wrangler.jsonc` with a real `id` and a `localConnectionString` for dev.
|
|
150
|
-
- [ ] `lunora codegen` run so `ctx.sql` is typed on `ActionCtx`.
|
|
151
|
-
- [ ] All `ctx.sql` access lives in actions (advisor clean — no
|
|
152
|
-
`hyperdrive_outside_action`).
|
|
153
|
-
- [ ] If reactivity is needed, external rows are projected into a `defineSchema`
|
|
154
|
-
table.
|
|
164
|
+
1. **`ctx.sql` in a query/mutation.** It's action-only. Move the SQL into an action.
|
|
165
|
+
2. **Expecting `ctx.sql` writes to update subscriptions.** They don't. Use Step 5.
|
|
166
|
+
3. **Placeholder or empty `id`.** The binding can't connect. Run
|
|
167
|
+
`wrangler hyperdrive create` and paste the id.
|
|
168
|
+
4. **Wrong placeholders.** Use `$1` for `postgres`/`pg` and `?` for `mysql2`.
|
|
169
|
+
5. **Unscoped `.source()` under `.shardBy()`.** Without `tenantBy`, every tenant's
|
|
170
|
+
rows are copied into each shard. `defineSchema` throws, and the
|
|
171
|
+
`external_source_unscoped` lint fails the build.
|