@tulipes/core 0.1.3 → 0.1.5
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/README.md +25 -10
- package/dist/boot/boot.d.ts +3 -3
- package/dist/boot/boot.js +12 -9
- package/dist/boot/boot.js.map +1 -1
- package/dist/boot/contracts.d.ts +2 -2
- package/dist/cli/init.d.ts +9 -7
- package/dist/cli/init.js +464 -67
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/main.js +1 -1
- package/dist/http/error-handler.js +21 -0
- package/dist/http/error-handler.js.map +1 -1
- package/dist/queues/queue-manager.d.ts +1 -1
- package/dist/queues/queue-manager.js +1 -1
- package/dist/sockets/load-sockets.d.ts +2 -2
- package/dist/sockets/load-sockets.js +1 -1
- package/dist/sockets/socket-manager.d.ts +1 -1
- package/dist/sockets/socket-manager.js +1 -1
- package/package.json +3 -2
- package/templates/CLAUDE.md +72 -0
- package/templates/claude/skills/tulipes-boot-errors/SKILL.md +64 -0
- package/templates/claude/skills/tulipes-endpoint/SKILL.md +82 -0
- package/templates/claude/skills/tulipes-env-variable/SKILL.md +78 -0
- package/templates/claude/skills/tulipes-model/SKILL.md +75 -0
- package/templates/claude/skills/tulipes-module/SKILL.md +60 -0
- package/templates/claude/skills/tulipes-permissions/SKILL.md +61 -0
- package/templates/claude/skills/tulipes-queue/SKILL.md +62 -0
- package/templates/claude/skills/tulipes-socket/SKILL.md +55 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tulipes-permissions
|
|
3
|
+
description: Use when adding roles or permissions to a Tulipes app, guarding an endpoint by role, or deciding who may do what. Covers module.acl.ts, the global role vocabulary and module-namespaced resources.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Adding permissions
|
|
7
|
+
|
|
8
|
+
Roles are **global**, declared once by the sys `core` module. Every other
|
|
9
|
+
module **grants** permissions on its own resources to those roles.
|
|
10
|
+
|
|
11
|
+
`modules/core/module.acl.ts` — the vocabulary:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type { AclBuilder } from "@tulipes/core/acl";
|
|
15
|
+
|
|
16
|
+
export default function coreAcl(acl: AclBuilder): void {
|
|
17
|
+
acl.defineRole("admin").defineRole("user");
|
|
18
|
+
acl.allow("admin", "*");
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`modules/billing/module.acl.ts` — grants for this feature:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import type { AclBuilder } from "@tulipes/core/acl";
|
|
26
|
+
|
|
27
|
+
export default function billingAcl(acl: AclBuilder): void {
|
|
28
|
+
acl.allow("user", "invoices:read");
|
|
29
|
+
acl.allow("accountant", "invoices:read", "invoices:write");
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Check at the edge of a handler:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
if (!acl!.can(role, "invoices:write")) throw new HttpError(403, "forbidden");
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
- **Resources are namespaced by module**: `invoices:read`,
|
|
42
|
+
`users:write`. Match the module's short name so ownership is obvious.
|
|
43
|
+
- **Grant, don't define.** A feature module calling `defineRole()` usually
|
|
44
|
+
means the role belongs in `core` instead. Introducing a role is
|
|
45
|
+
deliberate and explicit — `allow()` on an unknown role is a boot error,
|
|
46
|
+
never an implicit creation.
|
|
47
|
+
- The same role+resource granted by two modules crashes the boot. Since
|
|
48
|
+
resources are namespaced, that always means a duplicated resource name.
|
|
49
|
+
- Wildcards: `*` (everything), `invoices:*` (whole namespace). Give `*`
|
|
50
|
+
to `admin` only.
|
|
51
|
+
- Unknown roles always deny at runtime — the safe answer to "can this
|
|
52
|
+
unheard-of role do X?" is no.
|
|
53
|
+
|
|
54
|
+
## Gotchas
|
|
55
|
+
|
|
56
|
+
- Load order matters: roles must exist before grants, which is why `core`
|
|
57
|
+
is `tier: "sys", priority: 0`. If your module runs before the module
|
|
58
|
+
defining a role, add it to `dependsOn`.
|
|
59
|
+
- `acl.can()` answers *what the role may do*, not *who the caller is*.
|
|
60
|
+
Authentication (deciding the role) is your auth module's job.
|
|
61
|
+
- Roles are lowercase kebab-case; resources are `name:action`.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tulipes-queue
|
|
3
|
+
description: Use when adding background work to a Tulipes app — emails, webhooks, exports, scheduled retries — or when a request handler is doing something slow. Covers the queues contract, producing jobs, and the backend/worker process split.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Adding a background job
|
|
7
|
+
|
|
8
|
+
One file declares both sides of a queue, and the process mode decides what
|
|
9
|
+
happens with it: the **backend** process registers the queue so routes can
|
|
10
|
+
produce into it; the **worker** process turns the processor into a live
|
|
11
|
+
consumer. Never duplicate the definition.
|
|
12
|
+
|
|
13
|
+
`modules/billing/queues/invoices.queues.ts`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import type { Ctx } from "@tulipes/core/boot";
|
|
17
|
+
import type { QueueRegistry } from "@tulipes/core/queues";
|
|
18
|
+
|
|
19
|
+
export default function invoiceQueues({ models }: Ctx, queues: QueueRegistry): void {
|
|
20
|
+
queues.define("billing.send-invoice");
|
|
21
|
+
|
|
22
|
+
queues.process("billing.send-invoice", async (job) => {
|
|
23
|
+
const invoice = await models!.get("Invoice").findById(job.data.invoiceId);
|
|
24
|
+
// …do the slow thing here…
|
|
25
|
+
return { sent: true };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Produce from anywhere with the context:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
await queues!.add("billing.send-invoice", "send", { invoiceId: invoice.id });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run a consumer with `yarn worker`. Without it, jobs pile up in Redis and
|
|
37
|
+
nothing processes them — that is the correct behaviour, not a bug.
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
- **Queue names use `.`, never `:`** (`billing.send-invoice`). BullMQ
|
|
42
|
+
reserves `:` as its Redis key separator; a `:` name crashes the boot.
|
|
43
|
+
- Names are globally unique, and a queue may have only one processor —
|
|
44
|
+
duplicates crash the boot naming both modules.
|
|
45
|
+
- A processor registered for a queue nobody defined is a boot error; call
|
|
46
|
+
`define()` in the same file.
|
|
47
|
+
- Requires `REDIS_URL` declared (sys `core` module) and set. Queue files
|
|
48
|
+
without it are a boot refusal.
|
|
49
|
+
- Worker mode with zero processors refuses to start: a consumer with
|
|
50
|
+
nothing to consume is a misconfigured deployment.
|
|
51
|
+
|
|
52
|
+
## Writing a processor
|
|
53
|
+
|
|
54
|
+
- **Jobs retry.** Make the handler idempotent — the same job may run twice
|
|
55
|
+
after a crash or timeout.
|
|
56
|
+
- Pass identifiers in `job.data`, not whole documents; re-read from the
|
|
57
|
+
database inside the processor so the job acts on current state.
|
|
58
|
+
- Keep payloads small and JSON-serializable.
|
|
59
|
+
- Throwing marks the job failed and schedules a retry per its options; let
|
|
60
|
+
it throw rather than swallowing errors.
|
|
61
|
+
- Both processes run the full boot pipeline, so `models`, `config` and
|
|
62
|
+
`Environment` are all available inside a processor.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tulipes-socket
|
|
3
|
+
description: Use when adding realtime features to a Tulipes app — websockets, live updates, presence, notifications — or when emitting events from a route or background job. Covers the sockets contract and namespace ownership.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Adding realtime
|
|
7
|
+
|
|
8
|
+
Socket.IO namespaces are claimed by modules, one owner each, in
|
|
9
|
+
`modules/<name>/sockets/*.sockets.ts`. Websockets are opt-in: an app with
|
|
10
|
+
no socket files starts no Socket.IO server at all.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import type { Ctx } from "@tulipes/core/boot";
|
|
14
|
+
import type { SocketRegistry } from "@tulipes/core/sockets";
|
|
15
|
+
|
|
16
|
+
export default function billingSockets({ config }: Ctx, sockets: SocketRegistry): void {
|
|
17
|
+
sockets.namespace("/billing", (nsp) => {
|
|
18
|
+
nsp.on("connection", (socket) => {
|
|
19
|
+
socket.emit("ready", { plan: config.billing?.plan });
|
|
20
|
+
|
|
21
|
+
socket.on("subscribe", (invoiceId: string) => {
|
|
22
|
+
socket.join(`invoice:${invoiceId}`);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Emit from anywhere with the context — a route, a bootstrap task, a queue
|
|
30
|
+
processor in the backend process:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
ctx.sockets!.of("/billing").to(`invoice:${id}`).emit("invoice:paid", payload);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Rules
|
|
37
|
+
|
|
38
|
+
- **Namespaces are claimed once.** A second module claiming `/billing`
|
|
39
|
+
crashes the boot naming both.
|
|
40
|
+
- `sockets.of()` throws on a namespace nobody claimed. Socket.IO's raw
|
|
41
|
+
`io.of()` would silently create it and emit into the void — use the
|
|
42
|
+
framework accessor so typos surface.
|
|
43
|
+
- Sockets exist in **backend mode only**. `ctx.sockets` is undefined in the
|
|
44
|
+
worker process, so a queue processor cannot emit directly — publish to
|
|
45
|
+
Redis, or have the backend process subscribe.
|
|
46
|
+
- Namespace names are lowercase kebab-case with a leading slash.
|
|
47
|
+
|
|
48
|
+
## Gotchas
|
|
49
|
+
|
|
50
|
+
- Authenticate in namespace middleware (`nsp.use(...)`), not per event —
|
|
51
|
+
the framework's HTTP pipeline does not run for socket connections.
|
|
52
|
+
- Scaling past one backend process needs the Socket.IO Redis adapter;
|
|
53
|
+
otherwise an emit only reaches clients connected to that instance.
|
|
54
|
+
- Socket handlers run outside the request pipeline: no `HttpError`, no
|
|
55
|
+
request logging. Handle failures explicitly and emit an error event.
|