create-substrat 0.8.0 → 0.8.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/index.js +4 -4
- package/package.json +1 -1
- package/template/.substrat/playbook.md +12 -2
- package/template/AGENTS.md +22 -0
- package/template/src/worker.ts +5 -1
package/index.js
CHANGED
|
@@ -36,11 +36,11 @@ const TEMPLATE = join(HERE, 'template');
|
|
|
36
36
|
// The runtime packages release together off one version line (the changesets `fixed`
|
|
37
37
|
// group), so one constant is right for all of them. Engines do NOT share a line —
|
|
38
38
|
// each versions on its own, so one pin per engine, deliberately.
|
|
39
|
-
const SUBSTRAT = '^0.
|
|
40
|
-
const ENGINE_WORKORDER = '^0.
|
|
41
|
-
const ENGINE_INVOICING = '^0.9.
|
|
39
|
+
const SUBSTRAT = '^0.93.0';
|
|
40
|
+
const ENGINE_WORKORDER = '^0.10.1';
|
|
41
|
+
const ENGINE_INVOICING = '^0.9.9';
|
|
42
42
|
const BOUNDARY_LINT = '^0.2.0';
|
|
43
|
-
const DEV_ISSUER = '^0.1.
|
|
43
|
+
const DEV_ISSUER = '^0.1.7';
|
|
44
44
|
|
|
45
45
|
const DOCS = 'https://substrat.net';
|
|
46
46
|
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<!-- Adapted by hand from the substrat skill (.claude/skills/substrat/SKILL.md in substrat-run/substrat) — NOT emitted from it. The two diverge on purpose; tools/playbook-sync.mts fails CI when the skill moves so a human ports what belongs here. Edit this file directly. -->
|
|
1
2
|
# Playbook — build a vertical on Substrat
|
|
2
3
|
|
|
3
4
|
The always-on rules live in [`AGENTS.md`](../AGENTS.md); read them first. This playbook is
|
|
@@ -71,7 +72,13 @@ Every vertical gets this whether or not it uses a single engine:
|
|
|
71
72
|
- **Tenancy** — tenants and scopes, isolated at the database level. A scope is one
|
|
72
73
|
SQLite/DO database. Cross-tenant access is not a bug you avoid; there is no API for it.
|
|
73
74
|
- **Permissions** — roles, grants, entity-narrowed grants, and every decision carries a
|
|
74
|
-
proof path (why it was allowed).
|
|
75
|
+
proof path (why it was allowed). **Sharing is a kernel verb, not a table you design**:
|
|
76
|
+
an operation narrows a permission it already holds onto one entity, and withdraws it,
|
|
77
|
+
with `ctx.grant(principal, perm, entityRef)` / `ctx.revoke(principal, perm, entityRef)`
|
|
78
|
+
— entity-required, delegating (re-checks the caller's own decision), transactional with
|
|
79
|
+
the operation. Neither alternative is this: a `ctx.link` edge is permanent (not
|
|
80
|
+
revocable at all), and org membership is revocable but coarse-grained. Never mint an
|
|
81
|
+
org per domain row to get a revoke.
|
|
75
82
|
- **Events + audit** — every mutation emits a kernel-stamped event. Origin fields (tenant,
|
|
76
83
|
scope, actor, time) are stamped by the kernel; your code cannot mislabel one.
|
|
77
84
|
- **Migrations** — journaled per module, applied lazily per scope.
|
|
@@ -392,7 +399,10 @@ await host.provisionScope(actor, { tenantId: tenant, scopeId: scope, jurisdictio
|
|
|
392
399
|
|
|
393
400
|
Define roles **per tenant** from the engines' `PERM` + your keys, assign them, create seed
|
|
394
401
|
entities via `stub.invoke` (**never raw SQL**), give portal principals entity-narrowed
|
|
395
|
-
grants. Make it idempotent.
|
|
402
|
+
grants. Make it idempotent. Seed-time grants are the platform actor's verb; sharing a
|
|
403
|
+
**user** initiates at runtime is `ctx.grant` / `ctx.revoke` inside an operation (see the
|
|
404
|
+
`AGENTS.md` section on sharing, and the [todo demo](https://github.com/substrat-run/substrat/tree/main/demos/todo)'s
|
|
405
|
+
`src/module.ts` for the two calls in place).
|
|
396
406
|
|
|
397
407
|
### `test/scenario.test.ts`
|
|
398
408
|
|
package/template/AGENTS.md
CHANGED
|
@@ -142,6 +142,28 @@ consumers). Rules 1–5 are enforced mechanically by `boundary-lint`.
|
|
|
142
142
|
adapter **rejects** a `ctx.link` for an undeclared edge, so a missing one fails loudly.
|
|
143
143
|
This is also what lets a portal permission-walk reach the owner.
|
|
144
144
|
|
|
145
|
+
## Sharing is `ctx.grant` / `ctx.revoke`, not a table
|
|
146
|
+
|
|
147
|
+
When a person shares their own record with another person — and takes them off it again —
|
|
148
|
+
the operation narrows a permission it already holds onto that one entity:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
await ctx.grant(principal, PERM.listContribute, listRef(listId)); // share
|
|
152
|
+
await ctx.revoke(principal, PERM.listContribute, listRef(listId)); // un-share
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Entity-required (module code can never write a scope- or tenant-wide grant), delegating
|
|
156
|
+
(the caller's own decision on that entity is re-checked, so an operation can never hand out
|
|
157
|
+
more than it holds), and transactional with the operation. Every later `ctx.check` reads
|
|
158
|
+
the grant, so nothing else has to remember who may touch what.
|
|
159
|
+
|
|
160
|
+
Neither alternative is this, so you can tell a real absence from this one: a `ctx.link`
|
|
161
|
+
edge is **not revocable at all** — it is permanent — and org membership is revocable but
|
|
162
|
+
coarse-grained — a whole org, not one record. Never mint an org per domain row, or a
|
|
163
|
+
membership table consulted by hand in every handler, to get a revoke. The two-line
|
|
164
|
+
reference is the [todo demo](https://github.com/substrat-run/substrat/tree/main/demos/todo)
|
|
165
|
+
(`src/module.ts`, `todo/share-list` and `todo/revoke-share`).
|
|
166
|
+
|
|
145
167
|
## The gates — run them, believe them
|
|
146
168
|
|
|
147
169
|
```sh
|
package/template/src/worker.ts
CHANGED
|
@@ -121,7 +121,11 @@ interface Env {
|
|
|
121
121
|
function nodeFor(req: Request, env: Env): Node {
|
|
122
122
|
let routed;
|
|
123
123
|
try {
|
|
124
|
-
routed = readRoutedNode(req.headers, {
|
|
124
|
+
routed = readRoutedNode(req.headers, {
|
|
125
|
+
expectedSecret: env.ROUTER_SECRET,
|
|
126
|
+
// #966: an unsigned assertion is refused unless this is an un-routed dev instance.
|
|
127
|
+
allowUnsigned: env.ALLOW_DEV_NODE === 'true',
|
|
128
|
+
});
|
|
125
129
|
} catch (e) {
|
|
126
130
|
if (e instanceof RouterAssertionError) throw new HTTPException(400, { message: e.message });
|
|
127
131
|
throw e;
|