@lunora/cli 1.0.0-alpha.38 → 1.0.0-alpha.380

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.
Files changed (130) hide show
  1. package/LICENSE.md +33 -0
  2. package/README.md +1 -1
  3. package/dist/bin.mjs +2 -10
  4. package/dist/index.d.mts +1011 -374
  5. package/dist/index.d.ts +1011 -374
  6. package/dist/index.mjs +1 -19
  7. package/dist/packem_chunks/dispatch.mjs +1 -0
  8. package/dist/packem_chunks/handler.mjs +1 -160
  9. package/dist/packem_chunks/handler10.mjs +1 -16
  10. package/dist/packem_chunks/handler11.mjs +1 -22
  11. package/dist/packem_chunks/handler12.mjs +1 -192
  12. package/dist/packem_chunks/handler13.mjs +3 -131
  13. package/dist/packem_chunks/handler14.mjs +1 -65
  14. package/dist/packem_chunks/handler15.mjs +1 -58
  15. package/dist/packem_chunks/handler16.mjs +1 -80
  16. package/dist/packem_chunks/handler17.mjs +7 -43
  17. package/dist/packem_chunks/handler18.mjs +1 -105
  18. package/dist/packem_chunks/handler19.mjs +3 -170
  19. package/dist/packem_chunks/handler2.mjs +1 -114
  20. package/dist/packem_chunks/handler20.mjs +3 -94
  21. package/dist/packem_chunks/handler21.mjs +1 -94
  22. package/dist/packem_chunks/handler22.mjs +2 -0
  23. package/dist/packem_chunks/handler23.mjs +1 -0
  24. package/dist/packem_chunks/handler24.mjs +99 -0
  25. package/dist/packem_chunks/handler25.mjs +10 -0
  26. package/dist/packem_chunks/handler26.mjs +1 -0
  27. package/dist/packem_chunks/handler3.mjs +1 -204
  28. package/dist/packem_chunks/handler4.mjs +3 -33
  29. package/dist/packem_chunks/handler5.mjs +1 -49
  30. package/dist/packem_chunks/handler6.mjs +1 -91
  31. package/dist/packem_chunks/handler7.mjs +1 -42
  32. package/dist/packem_chunks/handler8.mjs +2 -174
  33. package/dist/packem_chunks/handler9.mjs +1 -315
  34. package/dist/packem_chunks/runCodegenCommand.mjs +4 -52
  35. package/dist/packem_chunks/runDeployCommand.mjs +11 -594
  36. package/dist/packem_chunks/runDevCommand.mjs +2 -0
  37. package/dist/packem_chunks/runInitCommand.mjs +164 -1432
  38. package/dist/packem_chunks/runResetCommand.mjs +1 -41
  39. package/dist/packem_chunks/runRpcCommand.mjs +1 -68
  40. package/dist/packem_shared/COMMANDS-Ba7ybcYb.mjs +1 -0
  41. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-CyIsI3S1.mjs +1 -0
  42. package/dist/packem_shared/EXIT_CODE-D_XCixv8.mjs +1 -0
  43. package/dist/packem_shared/admin-token-VdUnvnKW.mjs +1 -0
  44. package/dist/packem_shared/admin-url-DJB8P6Jh.mjs +1 -0
  45. package/dist/packem_shared/binding-manifest-file-CQ1eQYy1.mjs +2 -0
  46. package/dist/packem_shared/buildRegistryIndex-Dc8D8AR6.mjs +1 -0
  47. package/dist/packem_shared/catalog-CahzmDLV.mjs +1 -0
  48. package/dist/packem_shared/cli-B5fWPcrA.mjs +2 -0
  49. package/dist/packem_shared/cli-manifest-CRet0egA.mjs +1 -0
  50. package/dist/packem_shared/cloudflare-credentials-BVmZnDZk.mjs +1 -0
  51. package/dist/packem_shared/codegen-error-AmH54ofi.mjs +3 -0
  52. package/dist/packem_shared/command-CAXl2A1S.mjs +1 -0
  53. package/dist/packem_shared/commands-DgUDxkA8.mjs +13 -0
  54. package/dist/packem_shared/createLogger-Cl18I8AX.mjs +2 -0
  55. package/dist/packem_shared/createRecordingSpawner-Ch7zuQQE.mjs +1 -0
  56. package/dist/packem_shared/deploy-target-DCWwiuAe.mjs +1 -0
  57. package/dist/packem_shared/diffSnapshots-WWwx-KvZ.mjs +5 -0
  58. package/dist/packem_shared/docker-AqLAkOyY.mjs +1 -0
  59. package/dist/packem_shared/health-probe-Bw9JFfIy.mjs +1 -0
  60. package/dist/packem_shared/import-BRM1hs7j.mjs +12 -0
  61. package/dist/packem_shared/index-oHZBGBwB.mjs +1 -0
  62. package/dist/packem_shared/insertSchemaExtension-DuiV6cba.mjs +8 -0
  63. package/dist/packem_shared/lint-ignore-report-DKZagpqk.mjs +2 -0
  64. package/dist/packem_shared/open-url-EnKy--w-.mjs +1 -0
  65. package/dist/packem_shared/output-format-B4eQHi5y.mjs +2 -0
  66. package/dist/packem_shared/parseManifest-CwPTKdtS.mjs +1 -0
  67. package/dist/packem_shared/path-containment-CgxYZggb.mjs +1 -0
  68. package/dist/packem_shared/plan-Dl57T11P.mjs +5 -0
  69. package/dist/packem_shared/planDevCommand-D8sm56hO.mjs +1 -0
  70. package/dist/packem_shared/platform-diagnostics-Cn2g6Jh-.mjs +4 -0
  71. package/dist/packem_shared/prompt-cancelled-BvsNxg_Q.mjs +1 -0
  72. package/dist/packem_shared/render-lunora-error-C3OJY5wk.mjs +2 -0
  73. package/dist/packem_shared/resolve-BrsV4Qyc.mjs +5 -0
  74. package/dist/packem_shared/resolve-target-C_ZloTvd.mjs +1 -0
  75. package/dist/packem_shared/runAddCommand-DTvfp4h2.mjs +1 -0
  76. package/dist/packem_shared/runExportCommand-slAq5BCF.mjs +5 -0
  77. package/dist/packem_shared/runMigrateGenerateCommand-Ii_WMZEA.mjs +11 -0
  78. package/dist/packem_shared/schemaIrToSnapshot-BjK-0IRo.mjs +1 -0
  79. package/dist/packem_shared/secret-key-6bCftsxj.mjs +1 -0
  80. package/dist/packem_shared/shared-D-zCOmgY.mjs +1 -0
  81. package/dist/packem_shared/storage-DWhTX8eS.mjs +1 -0
  82. package/dist/packem_shared/tui-prompts-B3YwUhGw.mjs +4 -0
  83. package/dist/packem_shared/variables-CPYyJtsL.mjs +1 -0
  84. package/dist/packem_shared/wrangler-name-Dsk5K1f-.mjs +1 -0
  85. package/dist/packem_shared/wrangler-secrets-3chIVv3H.mjs +1 -0
  86. package/package.json +39 -19
  87. package/skills/README.md +35 -17
  88. package/skills/lunora/SKILL.md +154 -64
  89. package/skills/lunora-create-package/SKILL.md +97 -84
  90. package/skills/lunora-deploy/SKILL.md +175 -105
  91. package/skills/lunora-functions/SKILL.md +362 -56
  92. package/skills/lunora-migration-helper/SKILL.md +162 -137
  93. package/skills/lunora-performance-audit/SKILL.md +137 -95
  94. package/skills/lunora-quickstart/SKILL.md +137 -136
  95. package/skills/lunora-realtime/SKILL.md +140 -99
  96. package/skills/lunora-setup-auth/SKILL.md +162 -102
  97. package/skills/lunora-setup-hyperdrive/SKILL.md +114 -97
  98. package/skills/lunora-setup-hyperdrive-global/SKILL.md +93 -90
  99. package/skills/lunora-setup-mail/SKILL.md +85 -113
  100. package/skills/lunora-setup-scheduler/SKILL.md +132 -96
  101. package/skills/lunora-setup-storage/SKILL.md +189 -95
  102. package/dist/packem_chunks/planDevCommand.mjs +0 -541
  103. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +0 -397
  104. package/dist/packem_shared/COMMANDS-B0ftFD_3.mjs +0 -948
  105. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +0 -244
  106. package/dist/packem_shared/admin-url-4UzT-CI4.mjs +0 -19
  107. package/dist/packem_shared/api-spec-CtA6ilu4.mjs +0 -13
  108. package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +0 -38
  109. package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +0 -31
  110. package/dist/packem_shared/command-D3lB_4Az.mjs +0 -19
  111. package/dist/packem_shared/commands-B-gR09Z_.mjs +0 -845
  112. package/dist/packem_shared/createLogger-B40gPzQo.mjs +0 -78
  113. package/dist/packem_shared/createRecordingSpawner-DxI3mebw.mjs +0 -43
  114. package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +0 -61
  115. package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +0 -161
  116. package/dist/packem_shared/docker-hMQ97KSQ.mjs +0 -21
  117. package/dist/packem_shared/insertSchemaExtension-DAqbfr9Z.mjs +0 -64
  118. package/dist/packem_shared/open-url-Dfq6fAyT.mjs +0 -41
  119. package/dist/packem_shared/output-format-wUvAN6AL.mjs +0 -17
  120. package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
  121. package/dist/packem_shared/parseManifest--vZf2FY1.mjs +0 -94
  122. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +0 -9
  123. package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +0 -16
  124. package/dist/packem_shared/runAddCommand-bnY6-HKb.mjs +0 -4
  125. package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +0 -79
  126. package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +0 -43
  127. package/dist/packem_shared/storage-BIsph-Vk.mjs +0 -84
  128. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +0 -658
  129. package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +0 -12
  130. 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. Use for sign-up/sign-in, email/password,
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
- Wire authentication into a Lunora app using the `auth` registry item, which is
11
- built on `@lunora/auth` (a thin wrapper over
12
- [better-auth](https://www.better-auth.com)) with sessions persisted in
13
- `SessionDO` and identity tables in D1.
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
- ## When to Use
14
+ If the project has no Lunora backend yet, start with `lunora-quickstart`.
16
15
 
17
- - Adding sign-up / sign-in to a Lunora app.
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
- ## When Not to Use
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
- - The project has no Lunora backend yet — use `lunora-quickstart` first.
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
- ## Workflow
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
- 1. Add the base `auth` item.
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
- ## Step 1: Add the base item
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
- lunora registry add auth
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
- This:
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
- 1. Adds `@lunora/auth`, `@lunora/mail`, and `@lunora/server` to `package.json`
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
- ## Step 2: Mount the handler
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
- In your Worker entry, route `/api/auth/*` to the scaffolded handler (see the
56
- generated `lunora/auth/index.ts` README block). `createWorker` handles the rest
57
- of the RPC surface; the auth handler owns the better-auth endpoints.
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
- ## Step 3: Env vars and the D1 database
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
- | Var | Secret | Notes |
62
- | -------------------- | ------ | ------------------------------------------------------------------- |
63
- | `BETTER_AUTH_SECRET` | yes | Encryption secret, min 32 chars. `openssl rand -base64 32`. |
64
- | `BETTER_AUTH_URL` | no | Public base URL, e.g. `http://localhost:8787` in dev, your domain. |
65
- | `MAIL_FROM` | no | Sender for verification / reset mail. Captured in the dev Mail tab. |
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
- Create the D1 database and paste its id into the `DB` binding in
68
- `wrangler.jsonc`:
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
- ```bash
71
- wrangler d1 create my-app-db
72
- ```
94
+ `.auth()` appears on the builder only after codegen sees `@lunora/auth`
95
+ installed, so run `lunora codegen` first.
73
96
 
74
- The better-auth schema (user/session/account/verification tables) is **not**
75
- declared in `lunora/schema.ts` — it is managed by better-auth in D1. In dev,
76
- `ensureMigrated(auth)` auto-applies it; in production prefer
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
- Verification and password-reset emails are **captured into the Lunora Studio
82
- Mail tab** in dev with zero email setup. For real delivery, `lunora registry add
83
- mail` (adds the `SEND_EMAIL` binding) or set `RESEND_API_KEY`.
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: Add a provider (optional)
122
+ ## Step 4: Providers and add-ons (optional)
86
123
 
87
- Each provider item builds on the base `auth` item (`requires: ["auth"]`):
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 registry add auth-clerk # Clerk via better-auth genericOAuth
91
- lunora registry add auth-auth0 # Auth0 via better-auth genericOAuth
92
- lunora registry add auth-magic-link # passwordless magic-link (mail)
93
- lunora registry add auth-otp # passwordless email one-time-password
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
- Add the base `auth` item first (or let the registry resolve the `requires`
97
- dependency). OAuth items need the provider's client id/secret added to
98
- `.dev.vars`.
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, mutation, v } from "@lunora/server";
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
- For richer checks (org membership, roles), compose `withAuthPlugins(auth)` and
118
- call the better-auth server API — see the scaffolded `lunora/auth/index.ts`.
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
- ### In the UI (React)
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 { Authenticated, Unauthenticated, useAuth } from "@lunora/react";
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
- function Account() {
126
- const { user, signIn, signOut } = useAuth();
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
- `@lunora/react` also exports `AuthLoading` and `useAuthState` for the loading
147
- window before the session resolves.
148
-
149
- ## Common Pitfalls
150
-
151
- 1. **Declaring better-auth tables in `lunora/schema.ts`.** They live in D1 and
152
- are managed by better-auth — do not add them to `defineSchema`.
153
- 2. **Placeholder `database_id`.** The `DB` binding ships with a placeholder;
154
- `wrangler d1 create` + paste the id, then `lunora doctor` to confirm.
155
- 3. **Missing/short `BETTER_AUTH_SECRET`.** better-auth needs ≥32 chars;
156
- `@lunora/auth` surfaces a clear error when it is absent.
157
- 4. **Expecting prod email to "just work".** Dev captures mail into the Studio;
158
- production needs `mail` (the `SEND_EMAIL` binding) or `RESEND_API_KEY` and a
159
- verified sender domain.
160
-
161
- ## Checklist
162
-
163
- - [ ] `lunora registry add auth` run, `pnpm install` done.
164
- - [ ] Auth handler mounted in the Worker entry.
165
- - [ ] `BETTER_AUTH_SECRET` / `BETTER_AUTH_URL` / `MAIL_FROM` set in `.dev.vars`.
166
- - [ ] D1 database created and its id pasted into the `DB` binding (`lunora
167
- doctor` clean).
168
- - [ ] Provider item added if needed (Clerk / Auth0 / magic link / OTP).
169
- - [ ] Functions gate on `ctx.auth.userId`; UI uses the auth gates/`useAuth`.
170
- - [ ] Verified sign-in → session → an authenticated query round-trip.
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 from an action via Cloudflare Hyperdrive. Use for `@lunora/hyperdrive`, `ctx.sql`, `createHyperdrive` + `fromPostgresJs`/`fromNodePg`/`fromMysql2`, the `HYPERDRIVE` binding, `wrangler hyperdrive create`, and the action-only / non-reactive guardrails (live queries don't track external writes).
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
- Wire an **existing** Postgres/MySQL database into a Lunora app using
9
- `@lunora/hyperdrive`, which surfaces a [Cloudflare
10
- Hyperdrive](https://developers.cloudflare.com/hyperdrive/) binding's connection
11
- string and a driver-agnostic `ctx.sql` client — usable **only inside an
12
- `action`**.
13
-
14
- > **Integrate, don't replace.** Hyperdrive talks to a database Lunora has no
15
- > visibility into. Queries through `ctx.sql` are **non-deterministic** (forbidden
16
- > in `query`/`mutation`, enforced by the `hyperdrive_outside_action` advisor
17
- > lint) and external writes are **invisible to Lunora live queries** —
18
- > subscriptions will NOT re-run when external rows change. Use it to read/write a
19
- > legacy DB from an action; if you want that data reactive, write a projection
20
- > into a `defineSchema` DO/D1 table.
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
- # choose ONE driver (optional peer deps — none is bundled):
55
- pnpm add postgres # postgres.js → fromPostgresJs ($1, $2 placeholders)
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
- This prints an `id`. Add the binding to `wrangler.jsonc`, using
69
- `localConnectionString` so `lunora dev` connects to your DB directly (no edge
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 this binding (errors if `binding` is missing; warns if `id` is
85
- empty/placeholder — it can't connect), but it does **not** auto-write the `id`:
86
- that's a remote resource only `wrangler hyperdrive create` can mint. Importing
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 codegen sees `ctx.sql` used, it adds `sql: SqlClient` to **`ActionCtx`
96
- only** — never `QueryCtx`/`MutationCtx` — with a JSDoc restating the
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: Use `ctx.sql` from an action
66
+ ## Step 4: Wire the client on the app builder
100
67
 
101
- ```ts
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 { action, v } from "@lunora/server";
77
+ import { defineApp } from "../lunora/_generated/app.js";
106
78
 
107
- export const listLegacyOrders = action.input({ orgId: v.string() }).action(async ({ ctx, args: { orgId } }) => {
108
- const { connectionString } = createHyperdrive(ctx.env.HYPERDRIVE);
109
- ctx.sql = fromPostgresJs(postgres(connectionString));
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
- return ctx.sql.query<{ id: string; total: number }>("select id, total from orders where org = $1", [orgId]);
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
- The package never rewrites SQL — use your driver's native placeholders (`$1` for
116
- Postgres, `?` for MySQL).
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
- ## Step 5 (optional): Make it reactive via a projection
111
+ Pick one approach:
119
112
 
120
- External writes don't hit the change-feed, so a subscription won't re-fire on a
121
- Postgres change. To make external data reactive, write a projection into a
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
- // This write re-runs live queries reading `orders`:
128
- await ctx.runMutation("orders:upsert", { id: row.id, total: row.total });
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 by design; the
134
- `hyperdrive_outside_action` advisor lint flags it. Move the SQL into an
135
- action.
136
- 2. **Expecting external writes to be reactive.** They aren't — Lunora can't see
137
- them. Project into a `defineSchema` table if you need reactivity.
138
- 3. **Placeholder / empty `id`.** A Hyperdrive binding with no real `id` can't
139
- connect (Lunora warns). Run `wrangler hyperdrive create` and paste the id.
140
- 4. **Bundling a driver as a hard dependency.** Drivers are optional peer deps —
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.