create-m5kdev 0.32.0 → 0.32.2
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/package.json +1 -1
- package/templates/minimal-app/.cursor/rules/module-grants-guide.mdc +28 -2
- package/templates/minimal-app/.cursor/rules/module-service-guide.mdc +31 -12
- package/templates/minimal-app/AGENTS.md.tpl +9 -7
- package/templates/minimal-app/apps/server/AGENTS.md.tpl +7 -0
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +6 -6
package/package.json
CHANGED
|
@@ -35,7 +35,14 @@ Each `ResourceGrant` has four fields:
|
|
|
35
35
|
| `action` | `string` | The permission action (`"read"`, `"write"`, `"delete"`, `"publish"`) |
|
|
36
36
|
| `level` | `"user" \| "team" \| "organization"` | The scope level at which the grant applies |
|
|
37
37
|
| `role` | `string` | The role that receives this grant (`"owner"`, `"admin"`, `"member"`) |
|
|
38
|
-
| `access` | `"all" \| "own"
|
|
38
|
+
| `access` | `"all" \| "own" \| "org" \| "none"` | See Access modes below |
|
|
39
|
+
|
|
40
|
+
### Access modes
|
|
41
|
+
|
|
42
|
+
- `"all"` — any entity (no ownership/org check). Use for platform/user-level admins only.
|
|
43
|
+
- `"own"` — ownership check (`userId`/`memberId`, `teamId`, or `organizationId` depending on grant level).
|
|
44
|
+
- `"org"` — same-organization only (`entity.organizationId === actor.organizationId`). Prefer for org owner/admin instead of `"all"`.
|
|
45
|
+
- `"none"` — explicit deny for that role/action (clearer than omitting the action).
|
|
39
46
|
|
|
40
47
|
## Example
|
|
41
48
|
|
|
@@ -43,6 +50,12 @@ Each `ResourceGrant` has four fields:
|
|
|
43
50
|
import type { ResourceGrant } from "@m5kdev/backend/modules/base/base.grants";
|
|
44
51
|
|
|
45
52
|
export const postsGrants: ResourceGrant[] = [
|
|
53
|
+
{
|
|
54
|
+
action: "read",
|
|
55
|
+
level: "team",
|
|
56
|
+
role: "owner",
|
|
57
|
+
access: "own",
|
|
58
|
+
},
|
|
46
59
|
{
|
|
47
60
|
action: "write",
|
|
48
61
|
level: "team",
|
|
@@ -61,6 +74,12 @@ export const postsGrants: ResourceGrant[] = [
|
|
|
61
74
|
role: "owner",
|
|
62
75
|
access: "own",
|
|
63
76
|
},
|
|
77
|
+
{
|
|
78
|
+
action: "read",
|
|
79
|
+
level: "organization",
|
|
80
|
+
role: "owner",
|
|
81
|
+
access: "own",
|
|
82
|
+
},
|
|
64
83
|
{
|
|
65
84
|
action: "write",
|
|
66
85
|
level: "organization",
|
|
@@ -119,7 +138,14 @@ readonly update = this.procedure<UpdateInput>("update")
|
|
|
119
138
|
The `.access()` step supports two entity resolution modes:
|
|
120
139
|
|
|
121
140
|
- `entityStep` — reads entity from a previous `.use()` or `.loadResource()` step.
|
|
122
|
-
- `entities` — inline function returning `{ userId?, organizationId?, teamId? }`.
|
|
141
|
+
- `entities` — inline function returning `{ userId?, memberId?, organizationId?, teamId? }`.
|
|
142
|
+
|
|
143
|
+
Behavior:
|
|
144
|
+
|
|
145
|
+
- **Array entities** — soft-filtered to allowed rows (may be `[]`); filtered result is written back to `entityStep` state. Does not 403 the whole list.
|
|
146
|
+
- **Single entity** — `FORBIDDEN` when denied (e.g. cross-org update with `"org"` grants).
|
|
147
|
+
|
|
148
|
+
Prefer organization-level `"org"` (not `"all"`) for multi-tenant owner/admin roles so `findById` mutations cannot cross orgs.
|
|
123
149
|
|
|
124
150
|
## Service Choice
|
|
125
151
|
|
|
@@ -24,6 +24,14 @@ apps/<app>/server/src/modules/<module>/<module>.service.ts
|
|
|
24
24
|
- Implement service methods as `readonly` procedures using the `this.procedure("name")` builder.
|
|
25
25
|
- Propagate `ServerResultAsync` consistently with `err(...)`/`ok(...)` composition.
|
|
26
26
|
|
|
27
|
+
## Avoid Trivial Service Delegation
|
|
28
|
+
|
|
29
|
+
- Do not add a service method whose only behavior is calling another service.
|
|
30
|
+
- Call the owning service directly when no business rule, authorization, validation, orchestration, transaction boundary, or meaningful domain translation is added.
|
|
31
|
+
- Renaming the dependency method, repackaging arguments, constructing a prompt, or forwarding actor/context data alone does not justify a wrapper.
|
|
32
|
+
- A delegating method is justified only when the current service genuinely owns the use case and adds behavior or protects a boundary callers must not bypass.
|
|
33
|
+
- If removing the method and calling the dependency directly would lose nothing, do not add the method.
|
|
34
|
+
|
|
27
35
|
## Procedure Builder
|
|
28
36
|
|
|
29
37
|
Each procedure declares its schemas from the DTO, then auth, resource loading,
|
|
@@ -38,25 +46,32 @@ access control, and the handler:
|
|
|
38
46
|
|
|
39
47
|
## Procedure Patterns
|
|
40
48
|
|
|
41
|
-
### List —
|
|
49
|
+
### List — org context filter + optional access soft-filter
|
|
42
50
|
|
|
43
51
|
```typescript
|
|
44
52
|
readonly list = this.procedure("list")
|
|
45
53
|
.input(postSchemas.input.list)
|
|
46
54
|
.output(postSchemas.output.list)
|
|
47
|
-
.requireAuth()
|
|
48
|
-
.
|
|
49
|
-
|
|
55
|
+
.requireAuth("organization")
|
|
56
|
+
.addContextFilter(["organization"])
|
|
57
|
+
.loadResource("posts", ({ input }) =>
|
|
58
|
+
this.repository.posts.queryList(input, {
|
|
50
59
|
globalSearchColumns: ["title", "excerpt", "content"],
|
|
51
|
-
})
|
|
52
|
-
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
.access({ action: "read", entityStep: "posts" })
|
|
63
|
+
.handle(({ state }) => state.posts);
|
|
53
64
|
```
|
|
54
65
|
|
|
66
|
+
- Default list scoping: `addContextFilter(["organization"])`.
|
|
67
|
+
- When grants are member-own only, prefer `addContextFilter(["member", "organization"])`.
|
|
68
|
+
- `.access` on arrays soft-filters allowed rows (empty OK) and writes back to `state.posts`.
|
|
69
|
+
|
|
55
70
|
### Create with business logic
|
|
56
71
|
|
|
57
72
|
```typescript
|
|
58
73
|
readonly create = this.procedure("create")
|
|
59
|
-
.requireAuth()
|
|
74
|
+
.requireAuth("organization")
|
|
60
75
|
.input(postSchemas.input.create)
|
|
61
76
|
.output(postSchemas.output.single)
|
|
62
77
|
.access({ action: "write" })
|
|
@@ -67,8 +82,9 @@ readonly create = this.procedure("create")
|
|
|
67
82
|
if (uniqueSlug.isErr()) return err(uniqueSlug.error);
|
|
68
83
|
|
|
69
84
|
return this.repository.posts.create({
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
// Org-scoped assets: stamp memberId (+ organizationId). Personal resources: stamp userId.
|
|
86
|
+
memberId: ctx.actor.memberId,
|
|
87
|
+
organizationId: ctx.actor.organizationId,
|
|
72
88
|
title: input.title.trim(),
|
|
73
89
|
slug: uniqueSlug.value,
|
|
74
90
|
content: input.content.trim(),
|
|
@@ -81,7 +97,7 @@ readonly create = this.procedure("create")
|
|
|
81
97
|
|
|
82
98
|
```typescript
|
|
83
99
|
readonly update = this.procedure("update")
|
|
84
|
-
.requireAuth()
|
|
100
|
+
.requireAuth("organization")
|
|
85
101
|
.input(postSchemas.input.update)
|
|
86
102
|
.output(postSchemas.output.single)
|
|
87
103
|
.loadResource("post", ({ input }) => this.repository.posts.findById(input.id))
|
|
@@ -114,8 +130,9 @@ readonly softDelete = this.procedure("softDelete")
|
|
|
114
130
|
## Access Control Modes
|
|
115
131
|
|
|
116
132
|
- **`entityStep`** — references a value stored by `.loadResource()`/`.use()`;
|
|
117
|
-
the entity's `userId`, `organizationId`, and `teamId` are read automatically.
|
|
118
|
-
-
|
|
133
|
+
the entity's `userId`, `memberId`, `organizationId`, and `teamId` are read automatically.
|
|
134
|
+
Arrays are soft-filtered; singles return `FORBIDDEN` when denied. Filtered arrays are written back to the step state.
|
|
135
|
+
- **`entities`** — inline function returning `{ userId?, memberId?, organizationId?, teamId? }`
|
|
119
136
|
when the entity is not loaded into state.
|
|
120
137
|
|
|
121
138
|
## Permissions and Grants
|
|
@@ -123,6 +140,8 @@ readonly softDelete = this.procedure("softDelete")
|
|
|
123
140
|
- Keep action names aligned with grants (`read`, `write`, `delete`, `publish`).
|
|
124
141
|
- Do not use undeclared aliases (e.g. guarding `"update"` when grants define `"write"`).
|
|
125
142
|
- If a module never uses `.access()`, extend `BaseService` instead of `BasePermissionService`.
|
|
143
|
+
- Access modes: `"all"` (global), `"org"` (same organization), `"own"` (ownership), `"none"` (explicit deny).
|
|
144
|
+
- Prefer `"org"` for org owner/admin grants so `findById` updates cannot cross organizations.
|
|
126
145
|
|
|
127
146
|
## Best Practices
|
|
128
147
|
|
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
- `apps/webapp` holds the React app shell, routes, providers, and the `posts` feature.
|
|
14
14
|
- `apps/email` holds the email templates used by auth flows and local email delivery.
|
|
15
15
|
|
|
16
|
-
## Backend Conventions
|
|
17
|
-
|
|
18
|
-
- Keep repository, service, and transport layers separate inside each module.
|
|
19
|
-
- Register modules in `apps/server/src/app.ts` via `createBackendApp(config, [modules])`.
|
|
20
|
-
- Use `BaseModule` subclasses for app modules; the kernel wires repositories, services, and tRPC.
|
|
21
|
-
- Keep tRPC files focused on input/output wiring and delegate logic to services.
|
|
22
|
-
- Do not
|
|
16
|
+
## Backend Conventions
|
|
17
|
+
|
|
18
|
+
- Keep repository, service, and transport layers separate inside each module.
|
|
19
|
+
- Register modules in `apps/server/src/app.ts` via `createBackendApp(config, [modules])`.
|
|
20
|
+
- Use `BaseModule` subclasses for app modules; the kernel wires repositories, services, and tRPC.
|
|
21
|
+
- Keep tRPC files focused on input/output wiring and delegate logic to services.
|
|
22
|
+
- Do not add service methods that merely call another service. Call the owning service directly unless the method adds a business rule, authorization, validation, orchestration, a transaction boundary, or meaningful domain translation.
|
|
23
|
+
- Renaming a dependency method, repackaging arguments, constructing a prompt, or forwarding actor/context data alone does not justify a service wrapper.
|
|
24
|
+
- Do not create Drizzle migrations by hand. Use the scaffolded config and your project migration workflow later if you need generated migrations.
|
|
23
25
|
|
|
24
26
|
## Frontend Conventions
|
|
25
27
|
|
|
@@ -30,6 +30,13 @@ apps/server/src/modules/<module>/
|
|
|
30
30
|
- tRPC files own transport only and must delegate to services.
|
|
31
31
|
- Register modules in `apps/server/src/app.ts`; use `db.ts` and `lib/auth.ts` only for scripts that need direct DB or auth access.
|
|
32
32
|
|
|
33
|
+
## Avoid Trivial Service Delegation
|
|
34
|
+
|
|
35
|
+
- Do not add service methods that merely call another service.
|
|
36
|
+
- Call the owning service directly unless the method adds a business rule, authorization, validation, orchestration, a transaction boundary, or meaningful domain translation.
|
|
37
|
+
- Renaming a dependency method, repackaging arguments, constructing a prompt, or forwarding actor/context data alone does not justify a wrapper.
|
|
38
|
+
- If removing the method and calling the dependency directly would lose no behavior or boundary, do not add it.
|
|
39
|
+
|
|
33
40
|
## Workflow, Redis, and push notifications
|
|
34
41
|
|
|
35
42
|
- `WorkflowModule` and `NotificationModule` are registered in `app.ts`. Start **Redis** locally (`REDIS_URL`) before `pnpm dev` on the server, or background jobs will not run.
|
|
@@ -5,12 +5,12 @@ catalog:
|
|
|
5
5
|
"@heroui/react": 3.0.3
|
|
6
6
|
"@heroui/styles": 3.0.3
|
|
7
7
|
"@libsql/client": 0.17.4
|
|
8
|
-
"@m5kdev/backend": 0.32.
|
|
9
|
-
"@m5kdev/commons": 0.32.
|
|
10
|
-
"@m5kdev/config": 0.32.
|
|
11
|
-
"@m5kdev/email": 0.32.
|
|
12
|
-
"@m5kdev/frontend": 0.32.
|
|
13
|
-
"@m5kdev/web-ui": 0.32.
|
|
8
|
+
"@m5kdev/backend": 0.32.2
|
|
9
|
+
"@m5kdev/commons": 0.32.2
|
|
10
|
+
"@m5kdev/config": 0.32.2
|
|
11
|
+
"@m5kdev/email": 0.32.2
|
|
12
|
+
"@m5kdev/frontend": 0.32.2
|
|
13
|
+
"@m5kdev/web-ui": 0.32.2
|
|
14
14
|
"@playwright/test": 1.57.0
|
|
15
15
|
"@react-email/components": 1.0.1
|
|
16
16
|
"@tailwindcss/vite": 4.1.11
|