create-warlock 5.34.2 → 5.34.3
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/CHANGELOG.md +16 -0
- package/llms-full.txt +21 -28
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/skills/{create-a-warlock-project/SKILL.md → SKILL.md} +20 -27
- package/templates/toolchains/oxc/.oxlintrc.json +3 -1
- package/templates/warlock/.oxlintrc.json +3 -1
- package/templates/warlock/AGENTS.md +10 -10
- package/templates/warlock/skills/api-design/SKILL.md +110 -50
- package/templates/warlock/skills/code-standards/SKILL.md +48 -39
- package/templates/warlock/skills/data-and-persistence/SKILL.md +72 -57
- package/templates/warlock/skills/git-workflow/SKILL.md +9 -9
- package/templates/warlock/skills/module-boundaries/SKILL.md +2 -2
- package/templates/warlock/skills/observability-and-resilience/SKILL.md +8 -6
- package/templates/warlock/skills/security-baseline/SKILL.md +23 -22
- package/templates/warlock/skills/testing-strategy/SKILL.md +7 -7
- package/templates/warlock/skills/web-code-standards/SKILL.md +157 -60
- package/templates/warlock/src/app/auth/models/otp/otp.model.ts +9 -3
- package/templates/warlock/src/app/auth/services/otp.service.ts +18 -6
- package/templates/warlock/src/app/contact/controllers/contact.controller.ts +6 -0
- package/templates/warlock/src/app/locale/controllers/locale.controller.ts +6 -0
- package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +7 -0
- package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +14 -9
- package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +1 -0
- package/templates/warlock/src/app/posts/models/post/post.model.ts +6 -0
- package/templates/warlock/src/app/posts/resources/post.resource.ts +1 -1
- package/templates/warlock/src/app/posts/schema/update-post.schema.ts +1 -0
- package/templates/warlock/src/app/shared/utils/format-http-error-response.ts +41 -0
- package/templates/warlock/src/app/users/commands/hello-world.command.ts +2 -1
- package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +12 -1
- package/templates/warlock/src/app/users/controllers/list-users.controller.ts +24 -1
- package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +1 -0
- package/templates/warlock/src/app/users/models/user/user.model.ts +5 -0
- package/templates/warlock/src/app/users/repositories/users.repository.ts +2 -2
- package/templates/warlock/src/app/users/resources/user.resource.ts +1 -1
- package/templates/warlock/src/app/users/schema/list-users.schema.ts +1 -1
- package/templates/warlock/src/config/http.ts +6 -0
- package/templates/warlock/src/web/home/components/contact-field.tsx +18 -0
- package/templates/warlock/src/web/home/components/contact-section.tsx +5 -29
- package/templates/warlock/src/web/home/components/contact-submit-button.tsx +16 -0
- package/templates/warlock/src/web/home/hooks/use-contact-field.ts +9 -0
- package/templates/warlock/src/web/home/hooks/use-contact-form.ts +5 -8
- package/templates/warlock/src/web/home/hooks/use-contact-locale.ts +34 -0
- package/templates/warlock/src/web/home/hooks/use-contact-submit-button.ts +6 -0
- package/templates/warlock/src/web/home/index.page.tsx +1 -1
- package/templates/warlock/src/web/home/types/contact-controls.type.ts +13 -0
- package/templates/warlock/src/web/shared/utils/set-form-errors.ts +3 -1
- package/templates/warlock/src/web/home/components/contact-form-controls.tsx +0 -42
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: observability-and-resilience
|
|
3
|
-
description:
|
|
3
|
+
description: "Request correlation, structured logs, timeouts, retries, idempotency, health checks, metrics, shutdown and bounded queries. Use when adding external I/O or production instrumentation. Use security-baseline instead for secrets and sensitive data handling."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Observability & resilience
|
|
@@ -53,7 +53,7 @@ Set a default at the `Http` instance level; override per request when the endpoi
|
|
|
53
53
|
// ✅ instance-level default
|
|
54
54
|
const stripeClient = new Http({
|
|
55
55
|
baseURL: "https://api.stripe.com",
|
|
56
|
-
timeout: 10_000,
|
|
56
|
+
timeout: 10_000, // 10s — enough for most calls, short enough to fail fast
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
// ✅ per-request override for a known-slow call
|
|
@@ -132,12 +132,14 @@ Persist `key → result` with a TTL longer than your retry window (24h is typica
|
|
|
132
132
|
|
|
133
133
|
### 4.4 Pattern
|
|
134
134
|
|
|
135
|
+
This simplified service sketch also needs an atomic reservation and provider-side idempotency to prevent concurrent duplicate charges. At the HTTP boundary, require `Idempotency-Key` and run `middleware.idempotency()` after auth as described in the `api-design` skill.
|
|
136
|
+
|
|
135
137
|
```typescript
|
|
136
138
|
// ✅
|
|
137
139
|
export async function chargePaymentService(input, idempotencyKey) {
|
|
138
|
-
const cached = await idempotencyRepository.
|
|
140
|
+
const cached = await idempotencyRepository.findBy("key", idempotencyKey);
|
|
139
141
|
if (cached) {
|
|
140
|
-
return cached.result;
|
|
142
|
+
return cached.get("result");
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
const result = await stripeClient.post("/v1/charges", input);
|
|
@@ -261,11 +263,11 @@ These belong here because performance failures present as resilience failures
|
|
|
261
263
|
|
|
262
264
|
A list endpoint that fires one query per item is a bug. Either eager-load relations (Cascade's `.with("relation")`) or batch-fetch the related ids in a single query and join in-process.
|
|
263
265
|
|
|
264
|
-
A code review that lets an N+1 through is doing its job poorly. Add it to the review checklist (already in `
|
|
266
|
+
A code review that lets an N+1 through is doing its job poorly. Add it to the review checklist (already in the `api-design` skill).
|
|
265
267
|
|
|
266
268
|
### 9.2 Pagination always
|
|
267
269
|
|
|
268
|
-
See `
|
|
270
|
+
See the `api-design` skill § 3. No unbounded list ever, even if today's dataset is 12 rows.
|
|
269
271
|
|
|
270
272
|
### 9.3 Stream large payloads
|
|
271
273
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: security-baseline
|
|
3
|
-
description:
|
|
3
|
+
description: "Input schemas, authentication, authorization, secrets, safe queries, rate limits, password handling and log redaction. Use when code accepts external input or handles identity or credentials. Use api-design instead for HTTP response contracts."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Security baseline
|
|
@@ -10,7 +10,7 @@ description: 'Project-level security floor for Warlock.js apps — input validat
|
|
|
10
10
|
|
|
11
11
|
The non-negotiable security floor for any project on this template. Each rule maps to a real class of incident — most production breaches trace back to one of these eight being absent or sloppy.
|
|
12
12
|
|
|
13
|
-
> **Sub-agent rule:** Before writing any code that handles external input, identity, or secrets, read this file. Code-style rules live in `
|
|
13
|
+
> **Sub-agent rule:** Before writing any code that handles external input, identity, or secrets, read this file. Code-style rules live in the `code-standards` skill — this skill is about what the code must do, not how it must look.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -27,13 +27,13 @@ Anything that crosses the process boundary:
|
|
|
27
27
|
- Webhook payloads
|
|
28
28
|
- Queue messages
|
|
29
29
|
- Env vars (validated once at boot — see § 3)
|
|
30
|
-
- Database rows from a
|
|
30
|
+
- Database rows from a _different_ service (treat them as external if you don't own the writer)
|
|
31
31
|
|
|
32
32
|
### 1.2 Where validation runs
|
|
33
33
|
|
|
34
34
|
At the boundary, exactly once. Never inside services.
|
|
35
35
|
|
|
36
|
-
The Warlock pattern: schema lives in the module's `schema/` folder, the controller types itself with the schema and attaches it via `controller.validation.schema`. See `
|
|
36
|
+
The Warlock pattern: schema lives in the module's `schema/` folder, the controller types itself with the schema and attaches it via `controller.validation.schema`. See the `api-design` skill § 6 for the full controller shape.
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
39
|
// ✅ schema/create-order.schema.ts
|
|
@@ -56,10 +56,10 @@ export type CreateOrderSchema = Infer<typeof createOrderSchema>;
|
|
|
56
56
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
57
57
|
import { type CreateOrderSchema, createOrderSchema } from "../schema/create-order.schema";
|
|
58
58
|
|
|
59
|
-
export const createOrderController: GuardedRequestHandler<CreateOrderSchema> = async (
|
|
59
|
+
export const createOrderController: GuardedRequestHandler<CreateOrderSchema> = async ({
|
|
60
60
|
request,
|
|
61
61
|
response,
|
|
62
|
-
) => {
|
|
62
|
+
}) => {
|
|
63
63
|
const order = await placeOrderService(request.validated()); // already typed + safe
|
|
64
64
|
return response.success({ order });
|
|
65
65
|
};
|
|
@@ -80,7 +80,7 @@ The framework returns `422 Unprocessable Entity` with structured field errors au
|
|
|
80
80
|
|
|
81
81
|
- **Verify the signature first**, schema-validate second. A signature failure aborts before any other work.
|
|
82
82
|
- Reject requests older than the provider's replay window (typically 5 minutes).
|
|
83
|
-
- Idempotency-key the handler (see `
|
|
83
|
+
- Idempotency-key the handler (see the `observability-and-resilience` skill § 4) — providers retry.
|
|
84
84
|
|
|
85
85
|
---
|
|
86
86
|
|
|
@@ -92,18 +92,18 @@ AuthN proves who you are. AuthZ decides what you can do. Conflating them is how
|
|
|
92
92
|
|
|
93
93
|
```typescript
|
|
94
94
|
// ❌ trusting the client
|
|
95
|
-
const order = await
|
|
95
|
+
const order = await Order.findOne({ publicId: id, userId: request.input("userId") });
|
|
96
96
|
|
|
97
97
|
// ✅ identity from the authenticated session
|
|
98
98
|
const user = request.locals.user;
|
|
99
|
-
const order = await
|
|
99
|
+
const order = await Order.findOne({ publicId: id, userId: user.id });
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
Any field whose value would determine permission (`userId`, `organizationId`, `roleId`, `isAdmin`) is read from the authenticated session — never from the request body, query, or header.
|
|
103
103
|
|
|
104
104
|
### 2.2 Authentication via `@warlock.js/auth`
|
|
105
105
|
|
|
106
|
-
The project's auth stack: JWT access tokens
|
|
106
|
+
The project's auth stack: JWT access tokens and optional refresh tokens, configured in `src/config/auth.ts`. The starter uses `NO_EXPIRATION` for access tokens and disables refresh tokens; choose finite lifetimes and explicitly enable refresh when the application needs sessions. Password hashing via `bcryptjs`. The framework's `authService` (`@warlock.js/auth`) is the single entry point:
|
|
107
107
|
|
|
108
108
|
```typescript
|
|
109
109
|
// ✅ thin wrapper service — the framework does the work
|
|
@@ -124,13 +124,13 @@ Pattern: at the start of every mutating service, check ownership and role:
|
|
|
124
124
|
```typescript
|
|
125
125
|
// ✅
|
|
126
126
|
export async function updateOrderService(orderId: string, input, user) {
|
|
127
|
-
const order = await
|
|
127
|
+
const order = await Order.findOne({ publicId: orderId });
|
|
128
128
|
|
|
129
129
|
if (!order) {
|
|
130
130
|
throw new OrderNotFoundError(orderId);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
if (order.userId !== user.id && !user.isAdmin) {
|
|
133
|
+
if (order.get("userId") !== user.id && !user.isAdmin) {
|
|
134
134
|
throw new ForbiddenError("not your order");
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -168,7 +168,7 @@ const authConfigurations: AuthConfigurations = {
|
|
|
168
168
|
jwt: {
|
|
169
169
|
secret: env("JWT_SECRET"),
|
|
170
170
|
expiresIn: "1h",
|
|
171
|
-
refresh: {
|
|
171
|
+
refresh: {/* ... */},
|
|
172
172
|
},
|
|
173
173
|
};
|
|
174
174
|
```
|
|
@@ -184,10 +184,10 @@ The logger redaction layer is the safety net, but the first line of defence is n
|
|
|
184
184
|
log.info("auth", "login", "User authenticated", { user, password, token });
|
|
185
185
|
|
|
186
186
|
// ✅
|
|
187
|
-
log.info("auth", "login", "User authenticated", { userId: user.
|
|
187
|
+
log.info("auth", "login", "User authenticated", { userId: user.get("publicId") });
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
Configure `log.configure({ redact: { paths: ["password", "token", "*.secret", "*.apiKey"] } })` at startup — see `@warlock.js/logger
|
|
190
|
+
Configure `log.configure({ redact: { paths: ["password", "token", "*.secret", "*.apiKey"] } })` at startup — see the `@warlock.js/logger` skill's `redact-sensitive-log-fields` topic.
|
|
191
191
|
|
|
192
192
|
### 3.4 Token lifetimes
|
|
193
193
|
|
|
@@ -213,7 +213,7 @@ const orders = await ordersRepository.raw(`status = '${input.status}'`);
|
|
|
213
213
|
|
|
214
214
|
If a request body is `{ status: { $ne: null } }`, a naive `find({ status: input.status })` becomes a `$ne: null` match — returns every row. The seal schema is what prevents this: declaring `status: v.string()` rejects an object value at the boundary.
|
|
215
215
|
|
|
216
|
-
This applies to **query strings too**, not just bodies — a `?status[$ne]=` smuggled through the query is the same attack. That's why list endpoints validate the query with a schema and never pass raw `request.all()` to the repository (see `
|
|
216
|
+
This applies to **query strings too**, not just bodies — a `?status[$ne]=` smuggled through the query is the same attack. That's why list endpoints validate the query with a schema and never pass raw `request.all()` to the repository (see the `api-design` skill § 3.5).
|
|
217
217
|
|
|
218
218
|
This is a real reason § 1 is non-negotiable.
|
|
219
219
|
|
|
@@ -281,6 +281,7 @@ For `/auth/login` and `/auth/reset-password`, key the limiter on the **email or
|
|
|
281
281
|
### 6.4 Progressive lockout
|
|
282
282
|
|
|
283
283
|
After N failed login attempts:
|
|
284
|
+
|
|
284
285
|
- 3 failures → 1-minute delay
|
|
285
286
|
- 5 failures → 5-minute delay + email notification
|
|
286
287
|
- 10 failures → temporary account lock + manual review
|
|
@@ -291,13 +292,13 @@ Never silently succeed-and-fail (returning 200 with no body to hide whether the
|
|
|
291
292
|
|
|
292
293
|
## 7. Dependency hygiene
|
|
293
294
|
|
|
294
|
-
### 7.1 `
|
|
295
|
+
### 7.1 `npm audit` in CI
|
|
295
296
|
|
|
296
|
-
CI runs `
|
|
297
|
+
CI runs `npm audit --audit-level=high` on every PR. High and critical vulnerabilities block merge. Moderate goes on the backlog with a fix-by date.
|
|
297
298
|
|
|
298
299
|
### 7.2 Lockfile committed
|
|
299
300
|
|
|
300
|
-
`
|
|
301
|
+
`package-lock.json` is checked in. CI installs with `npm ci`.
|
|
301
302
|
|
|
302
303
|
### 7.3 No `latest` in `package.json`
|
|
303
304
|
|
|
@@ -311,7 +312,7 @@ Every dependency is future risk. Before adding a 5-line utility, ask whether it'
|
|
|
311
312
|
|
|
312
313
|
## 8. Logging and PII
|
|
313
314
|
|
|
314
|
-
See `
|
|
315
|
+
See the `code-standards` skill § 8 for the call signature. This is the **what not to log** list:
|
|
315
316
|
|
|
316
317
|
### 8.1 Never log
|
|
317
318
|
|
|
@@ -340,7 +341,7 @@ log.configure({
|
|
|
340
341
|
});
|
|
341
342
|
```
|
|
342
343
|
|
|
343
|
-
See `@warlock.js/logger
|
|
344
|
+
See the `@warlock.js/logger` skill's `redact-sensitive-log-fields` topic for the additive per-channel layer.
|
|
344
345
|
|
|
345
346
|
---
|
|
346
347
|
|
|
@@ -356,7 +357,7 @@ Before merging a change that touches input, identity, secrets, or persistence:
|
|
|
356
357
|
- [ ] All DB queries go through Cascade — no raw string-built queries
|
|
357
358
|
- [ ] Rate limit appropriate for the route family (default for general, aggressive for auth)
|
|
358
359
|
- [ ] No `.env` or secrets file in the commit
|
|
359
|
-
- [ ] `
|
|
360
|
+
- [ ] `npm audit --audit-level=high` passes
|
|
360
361
|
- [ ] Logger redaction config covers any new sensitive field introduced
|
|
361
362
|
- [ ] File uploads cap size, sniff MIME, server-generate the stored filename
|
|
362
363
|
- [ ] Webhooks verify signature before any other work
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: testing-strategy
|
|
3
|
-
description:
|
|
3
|
+
description: "Vitest test scope, co-located service and endpoint specs when testing is enabled, isolation, boundary mocks, integration tests and flaky-test policy. Use when planning or reviewing test coverage. Use git-workflow instead for CI and merge gates."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Testing strategy
|
|
@@ -32,7 +32,7 @@ Write the spec in the **same change** that adds the service or controller. A rev
|
|
|
32
32
|
|
|
33
33
|
### 0.2 If testing is NOT enabled
|
|
34
34
|
|
|
35
|
-
No `test` script and no vitest/vite config — e.g. a scaffold created without the `test` feature. **Do not** add tests or testing dependencies unprompted, and don't invent a `vitest.config`. If the project is outgrowing prototype stage,
|
|
35
|
+
No `test` script and no vitest/vite config — e.g. a scaffold created without the `test` feature. **Do not** add tests or testing dependencies unprompted, and don't invent a `vitest.config`. If the project is outgrowing prototype stage, _suggest_ enabling the `test` feature — but never scaffold a suite the project didn't ask for.
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
@@ -95,7 +95,7 @@ Not enforced numerically. Enforced culturally — when a reviewer sees ten new e
|
|
|
95
95
|
### 2.2 Don't test
|
|
96
96
|
|
|
97
97
|
- **The framework** — `@warlock.js/*` has its own tests. Don't re-test that `response.success()` returns 200.
|
|
98
|
-
- **Trivial getters / setters** — `cart.totalCents` returning `this.get<number>("
|
|
98
|
+
- **Trivial getters / setters** — `cart.totalCents` returning `this.get<number>("totalCents", 0)` doesn't need a test
|
|
99
99
|
- **One-line passthroughs** — `loginService(...)` just calling `authService.login(...)` doesn't need a unit test (an integration test of the wiring is better)
|
|
100
100
|
- **Generated code** — migrations, resource scaffolds
|
|
101
101
|
|
|
@@ -149,7 +149,7 @@ const result = await summarizeContentService(input, { provider: ai });
|
|
|
149
149
|
expect(result).toBe("mocked response");
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
-
See `@warlock.js/ai
|
|
152
|
+
See the `@warlock.js/ai` skill's `run-ai-agent` topic for the full mock surface.
|
|
153
153
|
|
|
154
154
|
### 4.3 Database — real, via testcontainers
|
|
155
155
|
|
|
@@ -179,7 +179,7 @@ In the test environment `setupTest()` installs fakes for mail, queue, cache, sto
|
|
|
179
179
|
```typescript
|
|
180
180
|
import { mailFake, queueFake } from "@warlock.js/core/tests";
|
|
181
181
|
|
|
182
|
-
const order = { id:
|
|
182
|
+
const order = { id: "550e8400-e29b-41d4-a716-446655440000" }; // what the code under test placed
|
|
183
183
|
|
|
184
184
|
mailFake.assertSent({ to: "ada@example.test" });
|
|
185
185
|
queueFake.assertPushed("invoices.send", { orderId: order.id });
|
|
@@ -235,7 +235,7 @@ Low coverage = guaranteed gaps. High coverage = no guarantee of correctness. Tre
|
|
|
235
235
|
|
|
236
236
|
Chasing the last 5% of coverage produces brittle tests over edge-case code that nobody cares about. Spend the time on tests that catch real regressions.
|
|
237
237
|
|
|
238
|
-
### 6.4 The `
|
|
238
|
+
### 6.4 The `npm run test:coverage` command
|
|
239
239
|
|
|
240
240
|
Runs Vitest with `@vitest/coverage-v8` (already in dependencies). Add coverage as a CI gate per the floors above once the suite is large enough.
|
|
241
241
|
|
|
@@ -337,4 +337,4 @@ Before merging a change that should include tests (testing enabled — see § 0)
|
|
|
337
337
|
- [ ] Test names describe behaviour, not implementation
|
|
338
338
|
- [ ] No `.only`, no `.skip` without a tracking ticket
|
|
339
339
|
- [ ] Snapshots reviewed line-by-line if updated
|
|
340
|
-
- [ ] `
|
|
340
|
+
- [ ] `npm run test --run` passes; `npm run test:coverage` doesn't drop the floor
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: web-code-standards
|
|
3
|
-
description:
|
|
4
|
-
Strict separation-of-concerns standards for Warlock.js src/web pages and
|
|
5
|
-
components: loaders delegate data and business rules to app services;
|
|
6
|
-
components delegate all behavior to separate custom-hook files.
|
|
7
|
-
Use alongside code-standards for web work; skip for backend-only changes.
|
|
3
|
+
description: "Warlock web pages and setup files, component and hook ownership, Mongez forms, accessible controls, locale behavior and UI kit choices. Use when building or reviewing src/web features. Use code-standards instead for shared TypeScript rules."
|
|
8
4
|
---
|
|
9
5
|
|
|
10
6
|
# Web code standards
|
|
11
7
|
|
|
12
8
|
**Applies to:** `src/web/**` in this project. Read
|
|
13
|
-
|
|
9
|
+
the `code-standards` skill for the shared TypeScript rules,
|
|
14
10
|
and the relevant `@warlock.js/web` skill before using a framework feature.
|
|
15
11
|
This skill adds the web-specific rules; it does not override security or
|
|
16
12
|
module-boundary requirements. When reviewing existing code, identify debt
|
|
@@ -45,7 +41,7 @@ ancestor and child dictionaries cannot redefine the same key. Keep legacy
|
|
|
45
41
|
Within a web feature, use `components/`, `hooks/`, and `utils/` for their
|
|
46
42
|
named purpose; do not create `lib/` as a catch-all. These folders have no
|
|
47
43
|
barrel `index.ts`: import the named component, hook, or helper from its own
|
|
48
|
-
file. Keep types in `types/` when a feature has several contracts, or in one
|
|
44
|
+
file. These exact imports keep client bundles lean and prevent server-only code from being pulled through a barrel. The `code-standards` skill allows sub-folder barrels only for `src/app/**` server modules. Keep types in `types/` when a feature has several contracts, or in one
|
|
49
45
|
`<feature>.types.ts` file when they are small. Do not duplicate a contract in
|
|
50
46
|
both places. Put component props in that dedicated type module by default;
|
|
51
47
|
keep a tiny private prop type beside a component only when extracting it would
|
|
@@ -54,13 +50,17 @@ make the feature harder to read.
|
|
|
54
50
|
## 1. Keep the page contract, make the implementation thin
|
|
55
51
|
|
|
56
52
|
Page logic lives in the matching `.setup.ts` file: `config`, `loader`,
|
|
57
|
-
`register`, and `action`/`actions`. UI
|
|
58
|
-
component
|
|
53
|
+
`register`, and `action`/`actions`. The UI file owns its named default
|
|
54
|
+
component; re-export a framework-required named component such as `ErrorBoundary`
|
|
55
|
+
from that component's own file. Pair `layout.tsx` with
|
|
59
56
|
`layout.setup.ts`, `root.tsx` with `root.setup.ts`, and `error.page.tsx` with
|
|
60
57
|
`error.setup.ts`. Use type-only imports from setup for loader/action inference.
|
|
61
58
|
The framework projects browser-safe `register()` code to the client. Put route,
|
|
62
59
|
cache, validation, middleware, metadata, and sitemap policy inside `config`;
|
|
63
|
-
metadata always belongs in `config.metadata`.
|
|
60
|
+
metadata always belongs in `config.metadata`. Do not add an empty setup file
|
|
61
|
+
when a component has no configuration, loader, action, or registration work.
|
|
62
|
+
The starter follows this boundary for root configuration, home route policy
|
|
63
|
+
and translations, and not-found metadata.
|
|
64
64
|
|
|
65
65
|
Use the config type that matches the module. A page can preserve loader data
|
|
66
66
|
inference for metadata by satisfying `PageConfig<typeof loader>`:
|
|
@@ -82,15 +82,18 @@ export const config = {
|
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
```tsx title="src/web/posts.page.tsx"
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
import type { PageProps } from "@warlock.js/web";
|
|
86
|
+
import type { loader } from "./posts.setup";
|
|
87
|
+
|
|
88
|
+
export default function PostsPage({ data }: PageProps<typeof loader>) {
|
|
89
|
+
return <main>{data.title}</main>;
|
|
87
90
|
}
|
|
88
91
|
```
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
Use `href(name, params, query)` for named page navigation. Generated declarations
|
|
94
|
+
in `.warlock/typings/web-routes.d.ts` check route names and required path
|
|
95
|
+
parameters when included in the app's TypeScript project; runtime route-table
|
|
96
|
+
validation still applies. Do not widen those contracts to strings or add casts.
|
|
94
97
|
|
|
95
98
|
Layouts use one `config satisfies LayoutConfig` object for `prefix`,
|
|
96
99
|
`middleware`, `metadata`, and a sitemap default. Roots use one
|
|
@@ -133,11 +136,22 @@ queries, aggregations, and domain transformations in the service. A service
|
|
|
133
136
|
must have a meaningful domain name and own the operation, not exist as a
|
|
134
137
|
generic pass-through for a repository method.
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
Give every component one responsibility and one reason to change. Define one
|
|
140
|
+
component per file, with a named default export. Extract substantive view
|
|
141
|
+
sections into cohesive components. Put hooks in `hooks/` and pure UI helpers
|
|
142
|
+
in `utils/`, one hook or helper per file, exported by name. A hook owns state,
|
|
143
|
+
effects, handlers, navigation, and client mutations; a utility has no React or
|
|
144
|
+
browser lifecycle dependency. A presentational component needs no hook.
|
|
145
|
+
|
|
146
|
+
Keep a small piece of logic or a private helper inline only when it is tightly
|
|
147
|
+
coupled to that one component and keeping it there leaves the file small and
|
|
148
|
+
focused. A component file past roughly 150 lines, a second component, or multiple
|
|
149
|
+
helpers is a split signal, not a mechanical line limit: split at ownership
|
|
150
|
+
boundaries, never into arbitrary chunks to meet a count. The one-component rule
|
|
151
|
+
still applies. Framework logic exports remain in setup even when delegated.
|
|
152
|
+
Keep feature CSS beside the feature and import it from the owning presentation
|
|
153
|
+
boundary; keep loading, error, empty, focus, reduced-motion, and locale behavior
|
|
154
|
+
with that boundary.
|
|
141
155
|
|
|
142
156
|
Preserve the framework's rendering boundary: `root.tsx` owns the document,
|
|
143
157
|
`#vessel` and `<Scripts />`; the loader runs on the server, while the rendered
|
|
@@ -153,35 +167,6 @@ output; the page composes `PostArticle`, `PostComments`, and `RelatedPosts`;
|
|
|
153
167
|
`use-post-comments.ts` owns subscription, state, and handlers for
|
|
154
168
|
`PostComments`. Each piece has one reason to change.
|
|
155
169
|
|
|
156
|
-
## Setup and component files
|
|
157
|
-
|
|
158
|
-
Use a setup file when a feature registers framework behavior. The setup file
|
|
159
|
-
owns its `register`, `config`, and `loader` exports; its matching component
|
|
160
|
-
only renders the supplied data. Keep loader inference type-only so it does
|
|
161
|
-
not pull server work into a browser component:
|
|
162
|
-
|
|
163
|
-
The default starter follows this boundary: `root.setup.ts` owns strict-mode
|
|
164
|
-
configuration, `home/index.setup.ts` owns route policy, its loader, and the
|
|
165
|
-
universal translations `register()`, and `404.setup.ts` owns not-found
|
|
166
|
-
metadata. Do not add an empty setup file when a component has no server
|
|
167
|
-
configuration, loader, or registration work.
|
|
168
|
-
|
|
169
|
-
```tsx
|
|
170
|
-
import type { PageProps } from "@warlock.js/web";
|
|
171
|
-
import type { loader } from "./posts.setup";
|
|
172
|
-
|
|
173
|
-
export default function PostsPage({ data }: PageProps<typeof loader>) {
|
|
174
|
-
return <PostsList posts={data.posts} />;
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Each React component file has one named default component. Export hooks,
|
|
179
|
-
utilities, and types by name. A component renders; its feature hook owns
|
|
180
|
-
state, effects, handlers, navigation, and client mutations. A utility stays
|
|
181
|
-
pure and has no React or browser lifecycle dependency. Keep feature CSS beside
|
|
182
|
-
the feature and import it from the owning presentation boundary; keep loading,
|
|
183
|
-
error, empty, focus, reduced-motion, and locale behavior with that boundary.
|
|
184
|
-
|
|
185
170
|
## Declaration spacing and local types
|
|
186
171
|
|
|
187
172
|
Put one blank line after the final import. Keep a short related declaration run
|
|
@@ -218,17 +203,17 @@ state starts unset, write `useState<ErrorText>()`; do not add a redundant
|
|
|
218
203
|
same validated, public data as the page. Keep private routes out of public
|
|
219
204
|
discovery, and do not generate database-backed assets on every GET.
|
|
220
205
|
|
|
221
|
-
## 3. Components render; custom hooks own
|
|
206
|
+
## 3. Components render; custom hooks own behavior
|
|
222
207
|
|
|
223
|
-
**
|
|
224
|
-
component
|
|
225
|
-
|
|
208
|
+
**Delegate behavior to the component's dedicated custom hook by default.** A
|
|
209
|
+
component declares props, calls that hook if needed, and renders JSX from its
|
|
210
|
+
props and the hook's returned values. Put `useState`,
|
|
226
211
|
`useEffect`, subscriptions, timers, browser API calls, derived-value
|
|
227
212
|
calculations, event handlers, mutations, and navigation decisions in a
|
|
228
213
|
separate custom-hook file. Keep pure domain calculations in `src/app`
|
|
229
214
|
services; a hook may call a small pure UI helper outside the component.
|
|
230
|
-
|
|
231
|
-
|
|
215
|
+
Use section 1's inline exception only for small, tightly coupled logic;
|
|
216
|
+
reuse or file growth means it belongs in its own hook or utility file.
|
|
232
217
|
JSX conditionals that only select which markup to show are presentation,
|
|
233
218
|
not a license to implement business rules in render.
|
|
234
219
|
|
|
@@ -272,6 +257,117 @@ enforce the same policy independently. Use a full navigation when an auth or
|
|
|
272
257
|
document boundary requires it, otherwise prefer the framework's navigation
|
|
273
258
|
and refresh primitives so state and loader data stay in sync.
|
|
274
259
|
|
|
260
|
+
### Use Mongez React Form for every form
|
|
261
|
+
|
|
262
|
+
Always use `Form`, `useFormControl`, and `useSubmitButton` from
|
|
263
|
+
`@mongez/react-form`; the web stack already uses this package, including the
|
|
264
|
+
starter's contact form. Do not add a second form library or manage field
|
|
265
|
+
validation and submission state in parallel React state. Keep those hooks in
|
|
266
|
+
the control's and submit button's dedicated hook files, following section 1.
|
|
267
|
+
|
|
268
|
+
Declare validation in a browser-safe `<Form schema={schema}>` (Seal implements
|
|
269
|
+
Standard Schema), a control's `schema`, or `useFormControl({ rules: [...] })`.
|
|
270
|
+
The `rules` array contains imported rule objects, never rule-name strings.
|
|
271
|
+
For example, `{ name: "email", type: "email", required: true,
|
|
272
|
+
rules: [requiredRule, emailRule] }` uses Mongez's rules: `requiredRule` needs
|
|
273
|
+
`required: true`, and `emailRule` needs the control's `type: "email"`.
|
|
274
|
+
Put `requiredRule` first. Schema issues map back to registered control names,
|
|
275
|
+
so keep schema paths and control names aligned.
|
|
276
|
+
|
|
277
|
+
Never use `required`, `pattern`, `min`, `max`, `minLength`, or `type="email"`
|
|
278
|
+
as browser validation, or rely on the browser's constraint UI. These may be
|
|
279
|
+
Mongez rule configuration props; do not spread raw control props onto DOM
|
|
280
|
+
inputs. Mongez v4's `Form` renders `noValidate` by default; keep it enabled
|
|
281
|
+
and preserve it if using a custom form host. A schema-backed email field can
|
|
282
|
+
render `type="text"` with `inputMode="email"` and `autoComplete="email"`,
|
|
283
|
+
leaving email validation entirely to the form.
|
|
284
|
+
|
|
285
|
+
Render the control's `error` through `getErrorProps()` and connect the field
|
|
286
|
+
with `getInputProps()` (or equivalent explicit wiring for a custom widget).
|
|
287
|
+
This keeps field identity, focus, blur validation, and error announcement in
|
|
288
|
+
the same form contract. For example, each block below is its own file:
|
|
289
|
+
|
|
290
|
+
```ts title="src/web/contact/contact.schema.ts"
|
|
291
|
+
import { v } from "@warlock.js/seal";
|
|
292
|
+
|
|
293
|
+
export const contactSchema = v.object({ email: v.email() });
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
```ts title="src/web/contact/hooks/use-contact-email.ts"
|
|
297
|
+
import { useFormControl } from "@mongez/react-form";
|
|
298
|
+
|
|
299
|
+
export function useContactEmail() {
|
|
300
|
+
return useFormControl({ name: "email" });
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```tsx title="src/web/contact/components/contact-email.tsx"
|
|
305
|
+
import { useContactEmail } from "../hooks/use-contact-email";
|
|
306
|
+
|
|
307
|
+
export default function ContactEmail() {
|
|
308
|
+
const { id, error, getInputProps, getErrorProps } = useContactEmail();
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<div>
|
|
312
|
+
<label htmlFor={id}>Email</label>
|
|
313
|
+
<input {...getInputProps({ type: "text", inputMode: "email", autoComplete: "email" })} />
|
|
314
|
+
{error ? <span {...getErrorProps()}>{error}</span> : null}
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Render `ContactEmail` inside `<Form schema={contactSchema} onSubmit={submit}>`;
|
|
321
|
+
the feature hook owns `submit`. The submit button's hook calls
|
|
322
|
+
`useSubmitButton()` and exposes `disabled` and `isSubmitting` to its component.
|
|
323
|
+
For API mutations, use `useSubmitForm` from `@warlock.js/web/form` in the
|
|
324
|
+
feature hook. With generated route declarations and a handler's `responseSchema`,
|
|
325
|
+
`useSubmitForm({ route })` infers the declared success body; `ApiResponse` from
|
|
326
|
+
`@warlock.js/web` reads a declared status body. Do not replace this inference
|
|
327
|
+
with casts. Server endpoints must still validate input independently.
|
|
328
|
+
|
|
329
|
+
### Build custom controls with the project's UI kit
|
|
330
|
+
|
|
331
|
+
Do not render native `<select>`, `<input type="checkbox">`,
|
|
332
|
+
`<input type="radio">`, or `<input type="date">` UI. Build a searchable
|
|
333
|
+
select/dropdown with a search input, a custom checkbox, custom radio
|
|
334
|
+
controls, and a custom date picker. Each field uses `useFormControl` through
|
|
335
|
+
its dedicated hook; a radio group registers one selected value for the group.
|
|
336
|
+
Connect value/checked changes, blur, disabled state, focus refs, validation,
|
|
337
|
+
and errors to that control. Configure a checkbox control with
|
|
338
|
+
`type: "checkbox"` and wire `checked` / `setChecked`. Adapt other widget
|
|
339
|
+
callbacks to `changeValue`; do not blindly spread DOM input props onto a kit
|
|
340
|
+
component with a different callback contract. Keep search text and popup state separate from
|
|
341
|
+
the selected form value. A kit's hidden input for submission is acceptable;
|
|
342
|
+
it must not display native UI or become a second validation source.
|
|
343
|
+
|
|
344
|
+
Check the existing styling stack and UI kit before building controls:
|
|
345
|
+
|
|
346
|
+
- With Tailwind and shadcn/ui already installed, compose its components:
|
|
347
|
+
Command + Popover for a searchable select, Checkbox, RadioGroup, and
|
|
348
|
+
Calendar + Popover for dates. Keep Mongez as the form owner rather than
|
|
349
|
+
copying a kit example that introduces another form library.
|
|
350
|
+
- With Tailwind but no shadcn/ui, or with no UI kit, ask the user which kit
|
|
351
|
+
to use **before installing anything**. Recommend shadcn/ui for Tailwind
|
|
352
|
+
projects because its composable accessible primitives fit the styling
|
|
353
|
+
stack. Explain the recommendation for other stacks and wait for the
|
|
354
|
+
user's choice; never silently add dependencies.
|
|
355
|
+
- With another established kit and no Tailwind, use its corresponding
|
|
356
|
+
accessible primitives and the same Mongez adapters.
|
|
357
|
+
|
|
358
|
+
Custom controls must preserve the accessibility native controls supplied.
|
|
359
|
+
Follow the WAI-ARIA APG [combobox/listbox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/),
|
|
360
|
+
[checkbox](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/),
|
|
361
|
+
[radiogroup](https://www.w3.org/WAI/ARIA/apg/patterns/radio/), and
|
|
362
|
+
[date picker dialog/grid](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/)
|
|
363
|
+
patterns. Preserve labels, visible focus, disabled/selected/checked states,
|
|
364
|
+
`aria-invalid`, and error descriptions linked to `getErrorProps()` output.
|
|
365
|
+
Support keyboard search and arrow navigation, Enter selection, Space toggling,
|
|
366
|
+
and Escape dismissal as appropriate. Manage popup entry and focus return;
|
|
367
|
+
a modal date dialog traps focus and its calendar grid supports day/month
|
|
368
|
+
keyboard navigation. Kit composition still needs correct labels, focus refs,
|
|
369
|
+
and announced errors; styling alone does not make a control accessible.
|
|
370
|
+
|
|
275
371
|
## 5. Ship usable, localizable presentation
|
|
276
372
|
|
|
277
373
|
- Use semantic landmarks, headings, buttons, links, and explicit form labels.
|
|
@@ -311,12 +407,13 @@ Before approving a web change, ask:
|
|
|
311
407
|
effects without untangling unrelated responsibilities?
|
|
312
408
|
2. Does every loader obtain data and business decisions from a named app
|
|
313
409
|
service, with models/repositories kept out of the web layer?
|
|
314
|
-
3. Does each
|
|
315
|
-
|
|
410
|
+
3. Does each component have one responsibility and its own file, with hooks
|
|
411
|
+
and helpers extracted except for the small, tightly coupled inline case?
|
|
316
412
|
4. Is the service query bounded, the resource shape intentional, and the
|
|
317
413
|
cache scope safe?
|
|
318
414
|
5. Do refresh, navigation, errors, retries, and cleanup leave the UI correct?
|
|
319
|
-
6.
|
|
320
|
-
|
|
415
|
+
6. Do forms use Mongez schema/rules and accessible custom controls, with
|
|
416
|
+
native validation disabled and the UI kit choice agreed before installation?
|
|
417
|
+
Are interactions translated to the degree the interface promises?
|
|
321
418
|
7. Are the relevant quality rules enforced or covered by focused tests,
|
|
322
419
|
rather than described only in comments?
|
|
@@ -33,9 +33,15 @@ export class OTP extends Model<OTPSchema> {
|
|
|
33
33
|
* Check if OTP is valid (not expired, not used, not max attempts)
|
|
34
34
|
*/
|
|
35
35
|
public get isValid(): boolean {
|
|
36
|
-
if (this.get("usedAt"))
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
if (this.get("usedAt")) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if ((this.get("attempts") ?? 0) >= (this.get("maxAttempts") ?? 0)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (new Date() > new Date(this.get("expiresAt"))) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
39
45
|
return true;
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -19,12 +19,24 @@ const DEFAULT_OTP_EXPIRATION: Duration = { minutes: 15 };
|
|
|
19
19
|
// Parse duration to milliseconds
|
|
20
20
|
function parseDurationToMs(duration: Duration): number {
|
|
21
21
|
let ms = 0;
|
|
22
|
-
if (duration.milliseconds)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (duration.
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
if (duration.milliseconds) {
|
|
23
|
+
ms += duration.milliseconds;
|
|
24
|
+
}
|
|
25
|
+
if (duration.seconds) {
|
|
26
|
+
ms += duration.seconds * 1000;
|
|
27
|
+
}
|
|
28
|
+
if (duration.minutes) {
|
|
29
|
+
ms += duration.minutes * 60 * 1000;
|
|
30
|
+
}
|
|
31
|
+
if (duration.hours) {
|
|
32
|
+
ms += duration.hours * 60 * 60 * 1000;
|
|
33
|
+
}
|
|
34
|
+
if (duration.days) {
|
|
35
|
+
ms += duration.days * 24 * 60 * 60 * 1000;
|
|
36
|
+
}
|
|
37
|
+
if (duration.weeks) {
|
|
38
|
+
ms += duration.weeks * 7 * 24 * 60 * 60 * 1000;
|
|
39
|
+
}
|
|
28
40
|
return ms;
|
|
29
41
|
}
|
|
30
42
|
|