@lunora/cli 1.0.0-alpha.35 → 1.0.0-alpha.351

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 +1007 -372
  5. package/dist/index.d.ts +1007 -372
  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 +2 -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 +1 -131
  13. package/dist/packem_chunks/handler14.mjs +1 -65
  14. package/dist/packem_chunks/handler15.mjs +3 -58
  15. package/dist/packem_chunks/handler16.mjs +1 -80
  16. package/dist/packem_chunks/handler17.mjs +1 -43
  17. package/dist/packem_chunks/handler18.mjs +1 -105
  18. package/dist/packem_chunks/handler19.mjs +7 -170
  19. package/dist/packem_chunks/handler2.mjs +1 -114
  20. package/dist/packem_chunks/handler20.mjs +1 -94
  21. package/dist/packem_chunks/handler21.mjs +1 -94
  22. package/dist/packem_chunks/handler22.mjs +3 -0
  23. package/dist/packem_chunks/handler23.mjs +3 -0
  24. package/dist/packem_chunks/handler24.mjs +1 -0
  25. package/dist/packem_chunks/handler25.mjs +2 -0
  26. package/dist/packem_chunks/handler26.mjs +1 -0
  27. package/dist/packem_chunks/handler27.mjs +99 -0
  28. package/dist/packem_chunks/handler28.mjs +10 -0
  29. package/dist/packem_chunks/handler3.mjs +1 -204
  30. package/dist/packem_chunks/handler4.mjs +1 -33
  31. package/dist/packem_chunks/handler5.mjs +1 -49
  32. package/dist/packem_chunks/handler6.mjs +3 -91
  33. package/dist/packem_chunks/handler7.mjs +1 -42
  34. package/dist/packem_chunks/handler8.mjs +1 -174
  35. package/dist/packem_chunks/handler9.mjs +1 -315
  36. package/dist/packem_chunks/runCodegenCommand.mjs +4 -52
  37. package/dist/packem_chunks/runDeployCommand.mjs +7 -594
  38. package/dist/packem_chunks/runDevCommand.mjs +2 -0
  39. package/dist/packem_chunks/runInitCommand.mjs +164 -1432
  40. package/dist/packem_chunks/runResetCommand.mjs +1 -41
  41. package/dist/packem_chunks/runRpcCommand.mjs +1 -68
  42. package/dist/packem_shared/COMMANDS-DRMcVnnw.mjs +1 -0
  43. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-CyIsI3S1.mjs +1 -0
  44. package/dist/packem_shared/EXIT_CODE-D_XCixv8.mjs +1 -0
  45. package/dist/packem_shared/admin-token-VdUnvnKW.mjs +1 -0
  46. package/dist/packem_shared/admin-url-DJB8P6Jh.mjs +1 -0
  47. package/dist/packem_shared/binding-manifest-file-CQ1eQYy1.mjs +2 -0
  48. package/dist/packem_shared/buildRegistryIndex-Dc8D8AR6.mjs +1 -0
  49. package/dist/packem_shared/catalog-CahzmDLV.mjs +1 -0
  50. package/dist/packem_shared/cli-CG_JgNKu.mjs +2 -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-Cg06JNO2.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-DpVxvYpL.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-Cod9Cgll.mjs +5 -0
  69. package/dist/packem_shared/planDevCommand-1uuaH06D.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-DUCSc7jQ.mjs +5 -0
  74. package/dist/packem_shared/resolve-target-C_ZloTvd.mjs +1 -0
  75. package/dist/packem_shared/runAddCommand-BsOrlyfD.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-DsMeUoEg.mjs +1 -0
  80. package/dist/packem_shared/shared-D-zCOmgY.mjs +1 -0
  81. package/dist/packem_shared/storage-ChJtQ9E4.mjs +1 -0
  82. package/dist/packem_shared/tui-prompts-B3YwUhGw.mjs +4 -0
  83. package/dist/packem_shared/variables-acxcvVQa.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 +123 -9
  89. package/skills/lunora-create-package/SKILL.md +4 -3
  90. package/skills/lunora-deploy/SKILL.md +50 -11
  91. package/skills/lunora-functions/SKILL.md +215 -15
  92. package/skills/lunora-migration-helper/SKILL.md +74 -21
  93. package/skills/lunora-performance-audit/SKILL.md +78 -9
  94. package/skills/lunora-quickstart/SKILL.md +95 -27
  95. package/skills/lunora-realtime/SKILL.md +73 -39
  96. package/skills/lunora-setup-auth/SKILL.md +33 -6
  97. package/skills/lunora-setup-hyperdrive/SKILL.md +33 -13
  98. package/skills/lunora-setup-hyperdrive-global/SKILL.md +5 -0
  99. package/skills/lunora-setup-mail/SKILL.md +34 -28
  100. package/skills/lunora-setup-scheduler/SKILL.md +18 -14
  101. package/skills/lunora-setup-storage/SKILL.md +187 -27
  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
@@ -9,8 +9,10 @@ description: Adds authentication to a Lunora app. Use for sign-up/sign-in, email
9
9
 
10
10
  Wire authentication into a Lunora app using the `auth` registry item, which is
11
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.
12
+ [better-auth](https://www.better-auth.com)) with identity **and session** tables
13
+ in D1. (`SessionDO` is a standalone TTL'd token store `@lunora/auth` never
14
+ calls — don't wire it for auth. The Durable Object option is `LunoraAuthDO` /
15
+ `.auth({ namespace })`, which holds the whole better-auth schema.)
14
16
 
15
17
  ## When to Use
16
18
 
@@ -104,7 +106,9 @@ dependency). OAuth items need the provider's client id/secret added to
104
106
  The runtime resolves the session and exposes the user on every context:
105
107
 
106
108
  ```ts
107
- import { LunoraError, mutation, v } from "@lunora/server";
109
+ import { LunoraError } from "lunorash/server";
110
+
111
+ import { mutation, v } from "#lunora/_generated/server.js";
108
112
 
109
113
  export const createDocument = mutation.input({ title: v.string() }).mutation(async ({ ctx, args: { title } }) => {
110
114
  if (!ctx.auth.userId) {
@@ -119,22 +123,35 @@ call the better-auth server API — see the scaffolded `lunora/auth/index.ts`.
119
123
 
120
124
  ### In the UI (React)
121
125
 
126
+ `useAuth()` returns exactly `{ setToken, status, token, user }` — there is no
127
+ `signIn` / `signOut`. Lunora owns the **token**, not the sign-in flow: run the
128
+ flow with the better-auth client (`authClient.signIn.email(...)`), then hand the
129
+ resulting JWT to `setToken`. `setToken(null)` signs out.
130
+
122
131
  ```tsx
123
132
  import { Authenticated, Unauthenticated, useAuth } from "@lunora/react";
124
133
 
134
+ import { authClient } from "./auth-client";
135
+
125
136
  function Account() {
126
- const { user, signIn, signOut } = useAuth();
137
+ const { setToken, user } = useAuth();
138
+
139
+ const signIn = async () => {
140
+ const { data } = await authClient.signIn.email({ email, password });
141
+
142
+ setToken(data?.token ?? null);
143
+ };
127
144
 
128
145
  return (
129
146
  <>
130
147
  <Authenticated>
131
148
  <span>Signed in as {user?.email}</span>
132
- <button type="button" onClick={() => signOut()}>
149
+ <button type="button" onClick={() => setToken(null)}>
133
150
  Sign out
134
151
  </button>
135
152
  </Authenticated>
136
153
  <Unauthenticated>
137
- <button type="button" onClick={() => signIn()}>
154
+ <button type="button" onClick={signIn}>
138
155
  Sign in
139
156
  </button>
140
157
  </Unauthenticated>
@@ -146,6 +163,16 @@ function Account() {
146
163
  `@lunora/react` also exports `AuthLoading` and `useAuthState` for the loading
147
164
  window before the session resolves.
148
165
 
166
+ **Branch on `status`, never on `user === null`.** `status` is one of
167
+ `"unauthenticated"`, `"loading"`, `"authenticated"` or `"unreachable"`, and the
168
+ same four values back the gates in every adapter (`@lunora/vue`,
169
+ `@lunora/solid`, `@lunora/svelte`, `@lunora/angular`). `"unreachable"` means the
170
+ credential is held but the session endpoint could not be reached — an offline
171
+ reload, or a transient 5xx. It gates as **authenticated**, so the app renders,
172
+ but `user` may still be `null`; a UI that treats that `null` as signed out shows
173
+ a signed-out screen to a signed-in user. The full contract is documented on
174
+ `AuthStatus` in `@lunora/client/auth`.
175
+
149
176
  ## Common Pitfalls
150
177
 
151
178
  1. **Declaring better-auth tables in `lunora/schema.ts`.** They live in D1 and
@@ -92,24 +92,41 @@ that's a remote resource only `wrangler hyperdrive create` can mint. Importing
92
92
  lunora codegen
93
93
  ```
94
94
 
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.
95
+ When codegen sees `ctx.sql` used, it adds `readonly sql: SqlClient` to
96
+ **`ActionCtx` only** — never `QueryCtx`/`MutationCtx` — with a JSDoc restating
97
+ the determinism/realtime caveat, and emits a `.hyperdrive()` method on the
98
+ generated app builder for you to supply the client (step 4).
98
99
 
99
- ## Step 4: Use `ctx.sql` from an action
100
+ ## Step 4: Wire the client once, on the app builder
100
101
 
101
- ```ts
102
+ `ctx.sql` is `readonly` — assigning to it inside a handler is a `TS2540`.
103
+ Codegen cannot build the client for you (turning a connection string into a
104
+ `SqlClient` needs the driver you chose), so it emits a config thunk you fill in
105
+ at the app level. It is called once per shard construction, not per request:
106
+
107
+ ```ts title="src/server/index.ts"
108
+ import type { HyperdriveLike } from "@lunora/hyperdrive";
102
109
  import { createHyperdrive, fromPostgresJs } from "@lunora/hyperdrive";
103
110
  import postgres from "postgres";
104
111
 
105
- import { action, v } from "@lunora/server";
112
+ import { defineApp } from "../lunora/_generated/app.js";
113
+
114
+ const app = defineApp<Env>()
115
+ .shard((env) => env.SHARD)
116
+ .hyperdrive((env) => fromPostgresJs(postgres(createHyperdrive(env.HYPERDRIVE as HyperdriveLike).connectionString)))
117
+ .build();
118
+
119
+ export const ShardDO = app.ShardDO;
120
+ ```
121
+
122
+ Then read it from any action:
106
123
 
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));
124
+ ```ts
125
+ import { action, v } from "@/lunora/_generated/server";
110
126
 
111
- return ctx.sql.query<{ id: string; total: number }>("select id, total from orders where org = $1", [orgId]);
112
- });
127
+ export const listLegacyOrders = action
128
+ .input({ orgId: v.string() })
129
+ .action(async ({ ctx, args: { orgId } }) => ctx.sql.query<{ id: string; total: number }>("select id, total from orders where org = $1", [orgId]));
113
130
  ```
114
131
 
115
132
  The package never rewrites SQL — use your driver's native placeholders (`$1` for
@@ -122,10 +139,13 @@ Postgres change. To make external data reactive, write a projection into a
122
139
  `defineSchema` table from the same action — that write _is_ tracked:
123
140
 
124
141
  ```ts
142
+ import { api } from "@/lunora/_generated/api";
143
+
125
144
  const [row] = await ctx.sql.query<{ id: string; total: number }>("select id, total from orders where id = $1", [id]);
126
145
 
127
- // This write re-runs live queries reading `orders`:
128
- await ctx.runMutation("orders:upsert", { id: row.id, total: row.total });
146
+ // This write re-runs live queries reading `orders`. Pass the generated
147
+ // reference — `ctx.run*` takes a reference, not a "file:fn" string.
148
+ await ctx.runMutation(api.orders.upsert, { id: row.id, total: row.total });
129
149
  ```
130
150
 
131
151
  ## Common Pitfalls
@@ -70,6 +70,11 @@ Add the binding to `wrangler.jsonc` (use `localConnectionString` for `lunora dev
70
70
  }
71
71
  ```
72
72
 
73
+ The binding is required, not optional: with no `hyperdrive` entry at all,
74
+ `lunora dev` and `lunora deploy` both refuse to start. The NAME is yours — the
75
+ validator only checks that some binding exists, because your `exec` selector is
76
+ what picks it.
77
+
73
78
  > **Read-your-writes:** point the Hyperdrive config at the **primary** (or pin
74
79
  > writes to it) so a write then immediate read isn't served by a stale replica.
75
80
 
@@ -7,8 +7,9 @@ description: Adds transactional email to a Lunora app. Use for sending mail (ver
7
7
 
8
8
  Wire transactional email into a Lunora app using the `mail` registry item, which
9
9
  is built on `@lunora/mail` (a Cloudflare Email Workers transport with
10
- header-injection-safe address handling) and exposes a `sendEmail` action plus a
11
- fire-and-forget `queueEmail` action. In dev, every send is captured into the
10
+ header-injection-safe address handling) and exposes a `sendEmail` `internalAction`
11
+ plus a fire-and-forget `queueEmail` `internalAction` — server-only, because a
12
+ client-callable general-purpose mailer is an open relay. In dev, every send is captured into the
12
13
  Studio Mail tab instead of going out.
13
14
 
14
15
  ## When to Use
@@ -21,14 +22,14 @@ Studio Mail tab instead of going out.
21
22
 
22
23
  - The project has no Lunora backend yet — use `lunora-quickstart` first.
23
24
  - Mail is already installed and you just want to send — call
24
- `ctx.runAction(api.mail.sendEmail, …)` or `client.action("mail/sendEmail", …)`.
25
+ `ctx.runAction(internal.mail.sendEmail, …)` from a server handler.
25
26
 
26
27
  ## Workflow
27
28
 
28
29
  1. Add the `mail` item.
29
30
  2. Configure the `SEND_EMAIL` binding (or a provider) and `MAIL_FROM`.
30
31
  3. Regenerate types with `lunora codegen`.
31
- 4. Send mail from a function (or the client); render a React template if needed.
32
+ 4. Send mail from a server function; render a React template if needed.
32
33
 
33
34
  ## Step 1: Add the item
34
35
 
@@ -40,8 +41,8 @@ This:
40
41
 
41
42
  1. Adds `@lunora/mail` and `@lunora/server` to `package.json` (run
42
43
  `pnpm install` afterwards).
43
- 2. Copies `lunora/mail/index.ts` (the `sendEmail` / `queueEmail` actions) into
44
- your project — it is **yours** to edit.
44
+ 2. Copies `lunora/mail/index.ts` (the `sendEmail` / `queueEmail`
45
+ **`internalAction`s**) into your project — it is **yours** to edit.
45
46
  3. Adds a `send_email` binding (`SEND_EMAIL`, with a `destination_address`
46
47
  placeholder) to `wrangler.jsonc` and scaffolds `MAIL_FROM` (the default
47
48
  sender) into `.dev.vars`.
@@ -71,24 +72,27 @@ verification and forgot-password mail — is intercepted and surfaced in the
71
72
  lunora codegen
72
73
  ```
73
74
 
74
- The functions surface in the generated `api` as `api.mail.sendEmail` and
75
- `api.mail.queueEmail`.
75
+ The functions surface in the generated **`internal`** (server-only) namespace as
76
+ `internal.mail.sendEmail` and `internal.mail.queueEmail` — they are deliberately
77
+ **not** in the client-reachable `api`.
76
78
 
77
79
  ## Step 4: Send mail
78
80
 
79
81
  ### From another function
80
82
 
81
- `sendEmail` is an **action** (sending is non-transactional network I/O). From a
82
- mutation, schedule it as a follow-up so the request is not blocked:
83
+ `sendEmail` is an **`internalAction`** (sending is non-transactional network
84
+ I/O). From a mutation, schedule it as a follow-up so the request is not blocked:
83
85
 
84
86
  ```ts
85
- import { mutation, v } from "@lunora/server";
87
+ import { internalMutation, v } from "#lunora/_generated/server.js";
86
88
 
87
- import { api } from "./_generated/api";
89
+ import { internal } from "./_generated/api";
88
90
 
89
- export const inviteUser = mutation.input({ email: v.string() }).mutation(async ({ ctx, args: { email } }) => {
90
- // ...persist the invite, then send the mail as a follow-up action
91
- await ctx.scheduler.runAfter(0, api.mail.sendEmail, {
91
+ export const inviteUser = internalMutation.input({ email: v.string() }).mutation(async ({ ctx, args: { email } }) => {
92
+ // ...authenticate the caller and persist the invite, then send the mail as a
93
+ // follow-up action. The recipient is decided server-side — never forward a
94
+ // client-chosen `to`/`from`/`html` straight through.
95
+ await ctx.scheduler.runAfter(0, internal.mail.sendEmail, {
92
96
  to: email,
93
97
  subject: "You're invited",
94
98
  html: "<p>Click the link to join.</p>",
@@ -96,15 +100,15 @@ export const inviteUser = mutation.input({ email: v.string() }).mutation(async (
96
100
  });
97
101
  ```
98
102
 
99
- ### From a client
103
+ ### Not from a client
100
104
 
101
- ```ts
102
- await client.action("mail/sendEmail", {
103
- to: "alice@example.com",
104
- subject: "Welcome",
105
- text: "Thanks for signing up!",
106
- });
107
- ```
105
+ There is no `client.action("mail/sendEmail", …)` path, and adding one is the
106
+ mistake this item exists to prevent: a general-purpose mailer that lets the
107
+ caller pick recipient, subject and body is an open relay for phishing through
108
+ your verified domain. If you need a client-callable send, write a
109
+ _purpose-specific_ public `action` that takes only safe business inputs (e.g.
110
+ `{ orderId }`), checks `ctx.auth`/RBAC, derives the recipient server-side,
111
+ rate-limits it (`@lunora/ratelimit`), and calls `internal.mail.sendEmail`.
108
112
 
109
113
  ### With a React email template
110
114
 
@@ -131,8 +135,10 @@ await createMailer({ apiKey: env.RESEND_API_KEY as string, from: env.MAIL_FROM a
131
135
  1. **Expecting prod email to "just work".** Dev captures into the Studio;
132
136
  production needs the `SEND_EMAIL` binding (a verified destination) or
133
137
  `RESEND_API_KEY`.
134
- 2. **Calling `sendEmail` as a query/mutation.** It is an action — invoke it via
135
- `ctx.runAction` / `ctx.scheduler.runAfter` / `client.action`, never `ctx.db`.
138
+ 2. **Calling `sendEmail` from the client, or as a query/mutation.** It is an
139
+ `internalAction` — invoke it via `ctx.runAction` / `ctx.scheduler.runAfter`
140
+ from a server handler. A client `client.action("mail/sendEmail", …)` is not
141
+ reachable and answers `FUNCTION_NOT_FOUND`.
136
142
  3. **Using `queueEmail` without a Queue binding.** It requires a Cloudflare
137
143
  Queue producer binding; until you add one, `@lunora/mail` throws
138
144
  `` `queue` binding is required for mailer.queue() ``. The item does not add
@@ -145,7 +151,7 @@ await createMailer({ apiKey: env.RESEND_API_KEY as string, from: env.MAIL_FROM a
145
151
  - [ ] `lunora registry add mail` run, `pnpm install` done.
146
152
  - [ ] `SEND_EMAIL` binding configured (verified destination) or
147
153
  `RESEND_API_KEY` set; `MAIL_FROM` set.
148
- - [ ] `lunora codegen` run so `api.mail.*` is generated.
149
- - [ ] Mail sent from a function (`ctx.scheduler.runAfter`/`ctx.runAction` with
150
- `api.mail.sendEmail`) or the client (`client.action("mail/sendEmail", …)`).
154
+ - [ ] `lunora codegen` run so `internal.mail.*` is generated.
155
+ - [ ] Mail sent from a server function (`ctx.scheduler.runAfter` / `ctx.runAction`
156
+ with `internal.mail.sendEmail`) — never from the client.
151
157
  - [ ] Verified the send appears in the Studio Mail tab in dev.
@@ -31,31 +31,36 @@ which owns the alarm and durable storage.
31
31
 
32
32
  ## Deferred dispatch — `runAfter` / `runAt`
33
33
 
34
- Available on `ctx.scheduler` in any function. Target functions are passed by
35
- reference from the generated `api` / `internal` proxy:
34
+ Available on `ctx.scheduler` in a **mutation or an action** — never a `query`,
35
+ which is deterministic and re-runs. Target functions are passed by reference
36
+ from the generated `api` / `internal` proxy (a `"file:fn"` path string also
37
+ works):
36
38
 
37
39
  ```ts
38
- import { mutation, v } from "@lunora/server";
40
+ import { mutation, v } from "#lunora/_generated/server.js";
39
41
 
40
42
  import { internal } from "./_generated/api";
41
43
 
42
44
  export const startTrial = mutation.input({ userId: v.string() }).mutation(async ({ ctx, args: { userId } }) => {
43
45
  // run an internal action 14 days from now
44
- const { id } = await ctx.scheduler.runAfter(14 * 24 * 60 * 60 * 1000, internal.billing.endTrial, { userId });
46
+ const jobId = await ctx.scheduler.runAfter(14 * 24 * 60 * 60 * 1000, internal.billing.endTrial, { userId });
45
47
 
46
- return { jobId: id };
48
+ return { jobId };
47
49
  });
48
50
  ```
49
51
 
50
- - `runAfter(delayMs, fnRef, args, options?)` — run after a delay (`delayMs` must
51
- be a non-negative finite number). `runAt(date, fnRef, args, options?)` — run at
52
- a `Date` or epoch-ms timestamp.
53
- - Both return `{ id, scheduledFor }`. Cancel with `ctx.scheduler.cancel(id)`;
52
+ - `runAfter(delayMs, fnRef, args?)` — run after a delay (`delayMs` must be a
53
+ non-negative finite number). `runAt(timestampMs, fnRef, args?)` — run at an
54
+ epoch-ms timestamp.
55
+ - Both resolve the job id. Cancel with `ctx.scheduler.cancel(id)`;
54
56
  inspect with `ctx.scheduler.get(id)` / `ctx.scheduler.list()`.
55
- - `options` accepts a `retry` policy (`{ maxAttempts, backoff, baseMs, maxMs }`;
56
- DO defaults: `maxAttempts: 5`, `backoff: "exponential"`, `baseMs: 30_000`) and
57
- a `shardKey` routing hint. On retry exhaustion the job is dead-lettered, never
58
- silently dropped.
57
+ - Those three arguments are the whole `ctx.scheduler` surface. Per-job
58
+ `RunOptions` — a `retry` policy (`{ maxAttempts, backoff, baseMs, maxMs }`), a
59
+ `shardKey` routing hint, `pool` — live on `@lunora/scheduler`'s own
60
+ `createScheduler(...)` client, which you construct yourself when you need them.
61
+ Jobs scheduled through `ctx.scheduler` take the DO defaults: 5 retries,
62
+ `backoff: "exponential"`, `baseMs: 30_000`. On retry exhaustion the job is
63
+ dead-lettered, never silently dropped.
59
64
  - The `SchedulerDO` binding (`SCHEDULER`) is **auto-inferred and reconciled**
60
65
  into `wrangler.jsonc` by `@lunora/config` once `@lunora/scheduler` is in use —
61
66
  run `lunora codegen` / `lunora doctor` to confirm. Scheduled jobs run with no
@@ -120,7 +125,6 @@ import { createWorkpool } from "@lunora/scheduler";
120
125
 
121
126
  const pool = createWorkpool({
122
127
  namespace: env.SCHEDULER,
123
- originUrl: "https://my-app.example.com",
124
128
  name: "imports",
125
129
  maxConcurrency: 3,
126
130
  });