@ismaelsoilet/synapsejs 1.8.0 → 1.11.1
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 +98 -6
- package/bin/synapse.ts +291 -241
- package/package.json +2 -2
- package/src/client/components.tsx +459 -1
- package/src/client/context.tsx +23 -3
- package/src/client/error-boundary.tsx +59 -0
- package/src/client/hooks.ts +100 -1
- package/src/client/toast.tsx +219 -0
- package/src/client/upload.ts +153 -0
- package/src/client.ts +37 -2
- package/src/compiler/client-bundler.ts +94 -2
- package/src/compiler/context-bench.ts +9 -1
- package/src/compiler/contract.ts +31 -4
- package/src/compiler/db-schema-generator.ts +39 -8
- package/src/compiler/fields-parser.ts +28 -1
- package/src/compiler/impact-analyzer.ts +31 -1
- package/src/compiler/migration-runner.ts +10 -2
- package/src/compiler/optional-coverage.ts +137 -0
- package/src/compiler/oracle-runner.ts +18 -4
- package/src/compiler/scaffolder.ts +227 -9
- package/src/compiler/slice-discovery.ts +8 -1
- package/src/compiler/slice-splitter.ts +68 -6
- package/src/compiler/slice-templates.ts +234 -10
- package/src/compiler/typescript-api.ts +33 -0
- package/src/core/action-context.ts +101 -15
- package/src/core/config.ts +88 -10
- package/src/core/cron-parser.ts +167 -0
- package/src/core/database-client.ts +103 -6
- package/src/core/index.ts +5 -0
- package/src/core/jobs.ts +37 -0
- package/src/core/oauth-client.ts +322 -0
- package/src/core/path-guard.ts +105 -0
- package/src/core/postgres-client.ts +37 -0
- package/src/core/postgres-queue.ts +76 -9
- package/src/core/query-builder.ts +166 -34
- package/src/core/session-context.ts +45 -4
- package/src/core/session-token.ts +124 -8
- package/src/core/sqlite-client.ts +20 -0
- package/src/core/storage.ts +60 -19
- package/src/core/topics.ts +84 -0
- package/src/core/webhook-signature.ts +151 -0
- package/src/index.ts +2 -1
- package/src/mcp/server.ts +317 -218
- package/src/mcp/tools.ts +329 -0
- package/src/project-status.ts +433 -0
- package/src/runtime/client-entry.ts +11 -5
- package/src/runtime/network-guard.ts +3 -1
- package/src/runtime/queue-engine.ts +167 -5
- package/src/runtime/rate-limiter.ts +14 -4
- package/src/runtime/security-headers.ts +35 -0
- package/src/runtime/server.ts +1174 -236
- package/src/runtime/uploads.ts +199 -19
- package/src/version.ts +9 -2
- package/templates/starter/package.json +2 -2
- package/templates/starter/tsconfig.json +1 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Features are listed with the command that fails when they break. Nothing is docu
|
|
|
21
21
|
without one — see the repository's `AGENTS.md` for the full contract.
|
|
22
22
|
|
|
23
23
|
**Stable:** vertical slices (N = 1) · `Result<T, E>` flows · TypeBox JIT contracts · declarative
|
|
24
|
-
`sliceSchema` migrations with DAG topological ordering · bidirectional `-- up:` / `-- down:` rollbacks (`synapse rollback`) · embedded SQLite (WAL, tuned busy_timeout & cache pragmas
|
|
24
|
+
`sliceSchema` migrations with DAG topological ordering · bidirectional `-- up:` / `-- down:` rollbacks (`synapse rollback`) · embedded SQLite (WAL, tuned busy_timeout & cache pragmas, CTE-safe; measured by `bun run bench:concurrency`) ·
|
|
25
25
|
full-duplex WebSockets (`defineSocket` + `useWebSocket`) · hierarchical sub-slice composition & outlets (`<SubSlice>`, `<SliceOutlet>`) · in-memory SSR micro-cache & ISR (`defineCache`) ·
|
|
26
26
|
vendor bundle code-splitting via Import Maps (`compiler.vendorChunks`) · standardized shared domain modules (`synapse new-shared`) ·
|
|
27
27
|
isomorphic Query Builder with parameterized relational joins and sub-object nesting (`nestJoinedRow`) · centralized Schema Catalog (`.codebase/db-schema.d.ts`) ·
|
|
@@ -33,7 +33,7 @@ with rawBody preservation · B2B multi-tenancy & IDOR prevention (`requireTenant
|
|
|
33
33
|
and Turbo Morphing router · explicit RBAC via `requireAuth(session, roles)` · zero-wiring routing + SSR + RPC · slice discovery
|
|
34
34
|
that never reports PASS with zero slices · isomorphic splitter with compile and leak gates · typed AST skeleton map ·
|
|
35
35
|
scaffolder with `--fields` grammar and templates (create, list, update, delete, login, oauth-github, crud) · standalone production bundler (`synapse build --standalone`) ·
|
|
36
|
-
MCP stdio server with
|
|
36
|
+
MCP stdio server with 23 native tools (16 Synapse + 7 Jev) · slice invariants under `bun:test` with per-invariant reporting · PostgreSQL parity · real concurrency & load stress benchmarks ·
|
|
37
37
|
SSRF & Network Guard (`validateExternalUrl`, `isPrivateOrReservedIp`) · anti-spoofing & bounded LRU rate limiting · bounded LRU SSR micro-cache with parameter normalization · strict payload size guards (RPC 5MB, Webhook 10MB) & malformed JSON protection · SQLite queue zombie job recovery with `locked_at` visibility timeout · HTTP 500 SSR error isolation & bundle suppression · secure HttpOnly cookies (`ctx.setCookie`) · graceful HTTP shutdown (`server.stop` with connection drain) ·
|
|
38
38
|
Prototype Pollution Shield (`setNestedProperty`, `expandNestedObject`) · Parameterized Query Operator Allowlist (`VALID_WHERE_OPERATORS`) · Reentrant SQLite Savepoints (`txDepth` + `SAVEPOINT`) · Atomic Queue Poison-Pill Neutralization (claim increment & DLQ timeout) · Strict Multi-Tenant Context Derivation (spoofing-proof token claims) · Streaming Upload & Chunked DoS Armor (`readBodyWithinLimit`) · Origin-Validated WebSocket Upgrades (`SYNAPSE_ALLOWED_ORIGINS`) · Safe SSR Serialization (`<script type="application/json">`) & Cookie CRLF Guard.
|
|
39
39
|
|
|
@@ -47,8 +47,10 @@ implemented with and without the slice convention, including what the numbers do
|
|
|
47
47
|
`fc` and `MockDatabaseClient` are deliberate parts of the contract, not accidents: slices declare their
|
|
48
48
|
invariants with `fc`, and the oracles use the mock. Both are asserted by
|
|
49
49
|
`packages/synapse/test/machine-types.test.ts` ("public contract"), together with every other export this
|
|
50
|
-
README lists. The same block is the list of
|
|
51
|
-
every break is in [CHANGELOG.md](
|
|
50
|
+
README lists. The same block is the list of frozen surfaces. A minor bump may still break in 1.x;
|
|
51
|
+
every break is listed in [CHANGELOG.md](../../CHANGELOG.md), each with the flag that restores the
|
|
52
|
+
previous behaviour where one exists — start with
|
|
53
|
+
[`docs/migration-1.9.md`](../../docs/migration-1.9.md) if you are coming from 1.8.
|
|
52
54
|
|
|
53
55
|
## Quick start
|
|
54
56
|
|
|
@@ -129,11 +131,101 @@ bun run mcp # JSON-RPC 2.0 over stdio
|
|
|
129
131
|
```
|
|
130
132
|
|
|
131
133
|
Tools: `synapse_get_repo_map`, `synapse_get_db_schema`, `synapse_check`, `synapse_split`,
|
|
132
|
-
`synapse_run_pbt`, `synapse_scaffold_slice`, `synapse_migrate`, `synapse_rollback`, `synapse_contract`,
|
|
134
|
+
`synapse_run_pbt`, `synapse_scaffold_slice`, `synapse_scaffold_domain`, `synapse_migrate`, `synapse_rollback`, `synapse_contract`,
|
|
133
135
|
`synapse_check_db_drift`, `synapse_diff_impact`, `synapse_test_gate`, `synapse_abort_check`,
|
|
134
|
-
`
|
|
136
|
+
`synapse_verify_completion`, `synapse_reasoning_effort`, `synapse_triage_error`, `synapse_route_task`, `synapse_evaluate_nudge`,
|
|
137
|
+
`synapse_inspect_routes`, `synapse_test_slice`, `synapse_generate_seed`, `synapse_scaffold_component`.
|
|
135
138
|
Slice-dependent tools fail with `NO_SLICES_DIR` (listing the paths they examined) instead of reporting an empty success.
|
|
136
139
|
|
|
140
|
+
## Write-surface containment
|
|
141
|
+
|
|
142
|
+
Every surface that turns caller input into a filesystem path segment goes through the single
|
|
143
|
+
validator in `core/path-guard.ts`: `synapse new-slice` (`domain` and `name`), `synapse new-shared`
|
|
144
|
+
(`name`), the CRUD scaffold, `synapse new`'s project directory and the upload parser. It rejects
|
|
145
|
+
separators, parent traversal, absolute prefixes, empty segments and over-length input, and the
|
|
146
|
+
resolved target must stay inside the directory the operation owns before anything is written.
|
|
147
|
+
`--fields` names are validated as plain identifiers before they reach the generated DDL, so a crafted
|
|
148
|
+
name cannot end up in a migration.
|
|
149
|
+
|
|
150
|
+
The failures are machine-readable and non-zero:
|
|
151
|
+
|
|
152
|
+
| code | meaning |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `INVALID_PATH_SEGMENT` | a domain, slice name, shared-module name or project directory is not a single safe path segment |
|
|
155
|
+
| `INVALID_FIELD_NAME` | a `--fields` name or enum value is not a plain identifier |
|
|
156
|
+
| `SLICE_EXISTS` | the target file already exists |
|
|
157
|
+
| `WRITE_FAILED` | the filesystem refused the write |
|
|
158
|
+
| `NO_SLICES_DIR` | no `src/slices` was found; `candidates[]` lists every path examined |
|
|
159
|
+
| `AMBIGUOUS_SLICES_DIR` | several workspace members own slices; set `SYNAPSE_ROOT` or run inside the target app |
|
|
160
|
+
|
|
161
|
+
Uploads apply the same name rule and additionally refuse a file whose declared extension is outside
|
|
162
|
+
`SYNAPSE_UPLOAD_ALLOWED_TYPES` (default: png, jpg, jpeg, gif, webp, pdf, txt, csv, json, md) or whose
|
|
163
|
+
bytes do not match that extension, answering `415 UNSUPPORTED_FILE_TYPE`. The framework does not serve
|
|
164
|
+
the upload directory — serving it is the application's responsibility.
|
|
165
|
+
|
|
166
|
+
## Sessions and cookies
|
|
167
|
+
|
|
168
|
+
With `SYNAPSE_SESSION_SECRET` set, a bearer token must be a signed session and its claims win: role
|
|
169
|
+
headers are ignored and the tenant comes exclusively from the verified claims. Revocation is consulted
|
|
170
|
+
on every request — in-process first, then against the persisted blacklist. Without a secret, identity
|
|
171
|
+
headers are honoured **only** under the explicit development opt-in `SYNAPSE_DEV_HEADERS=true` (or a
|
|
172
|
+
`development`/`test` runtime mode); an unset, staging or production environment resolves every request
|
|
173
|
+
to an anonymous session, and `synapse start` refuses to run in production without a secret unless the
|
|
174
|
+
operator passes `--acknowledge-insecure`.
|
|
175
|
+
|
|
176
|
+
`ctx.setCookie` is secure by default: a cookie written with no options carries `HttpOnly`, `Secure`
|
|
177
|
+
and `SameSite=Lax`, and only an explicit `false` opts out. A cookie written by **browser** script
|
|
178
|
+
(`storeSession`) cannot: `document.cookie` cannot set `HttpOnly`, by construction, so a
|
|
179
|
+
script-written session cookie is readable by any script in the origin. A session that must be
|
|
180
|
+
unreachable by script is the one the server emits through `ctx.setCookie`.
|
|
181
|
+
|
|
182
|
+
## Realtime: declared topics and a fail-closed upgrade
|
|
183
|
+
|
|
184
|
+
A topic is declared, never inferred — a name is not a permission. Declare it in the slice that owns it:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { defineTopic } from 'synapsejs';
|
|
188
|
+
|
|
189
|
+
export const sliceRealtime = defineTopic({ name: 'tickets', owner: 'tickets/view-tickets', readRoles: ['support'] });
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`GET /_synapse/sse/tickets` then refuses an unauthenticated subscriber (`401 UNAUTHENTICATED`), an
|
|
193
|
+
undeclared topic (`404 TOPIC_NOT_FOUND`) and a session without a declared role
|
|
194
|
+
(`403 TOPIC_FORBIDDEN`). Topics resolve inside the subscriber's tenant, so two tenants using the same
|
|
195
|
+
name never see each other's events, and `ctx.broadcast(topic, data)` notifies zero subscribers unless
|
|
196
|
+
the calling slice is the declared owner. The registry and concurrent subscriptions are bounded
|
|
197
|
+
(`config.realtime`), and a disconnect releases the topic entry.
|
|
198
|
+
|
|
199
|
+
The WebSocket upgrade (`GET /_synapse/ws/<domain>/<name>`) is fail-closed on origin: with no
|
|
200
|
+
`SYNAPSE_ALLOWED_ORIGINS` configured every upgrade is refused, an absent or opaque `null` origin is
|
|
201
|
+
always refused, and only a loopback origin is carved out for local development. Both transports share
|
|
202
|
+
one rate limiter and a per-client cap on concurrent connections. Every refusal is JSON
|
|
203
|
+
(`{ ok: false, error, message }`) with one of `UNAUTHENTICATED`, `TOPIC_NOT_FOUND`,
|
|
204
|
+
`TOPIC_FORBIDDEN`, `RATE_LIMIT_EXCEEDED`, `REALTIME_LIMIT_EXCEEDED`, `ORIGIN_NOT_ALLOWED` — the same
|
|
205
|
+
set `synapse contract` enumerates with its statuses.
|
|
206
|
+
|
|
207
|
+
## Per-process state, rate limiting and the image allowlist
|
|
208
|
+
|
|
209
|
+
The token bucket rate limiter, the server-render micro-cache and the realtime event hub keep their
|
|
210
|
+
state **in the serving process only**; there is no shared backend. A restart discards counters, cached
|
|
211
|
+
responses and buffered events, and N instances behind a load balancer permit up to N times the
|
|
212
|
+
per-instance allowance with a per-instance cache hit rate. The allowance registry is bounded
|
|
213
|
+
(`config.rateLimit.maxBuckets`, 10,000 by default) and a saturated registry **refuses** the excess
|
|
214
|
+
with `429` instead of evicting a live counter, so a rotating-identity flood cannot reset an abuser's
|
|
215
|
+
allowance. Forwarding headers are trusted only when `SYNAPSE_TRUST_PROXY=true` (or
|
|
216
|
+
`config.trustProxy`) is set; otherwise the peer address is the client identity, and every accepting
|
|
217
|
+
surface — RPC, uploads, webhooks, SSE, WebSocket upgrades, the image endpoint, health, metrics,
|
|
218
|
+
repo-map and server-rendered pages — draws from that one limiter.
|
|
219
|
+
|
|
220
|
+
`/_synapse/images/optimize` requires a session, is rate limited, caps the upstream response
|
|
221
|
+
(`config.imageOptimizer.maxResponseBytes`, 10 MiB by default) and the fetch duration
|
|
222
|
+
(`config.imageOptimizer.timeoutMs`, 10 s by default), connects to the exact address that passed
|
|
223
|
+
`validateExternalUrl` (so the hostname is not resolved a second time) and refuses redirects. When
|
|
224
|
+
`SYNAPSE_ALLOWED_IMAGE_DOMAINS` (or `config.imageOptimizer.allowedDomains`) lists hosts, only those
|
|
225
|
+
hosts are accepted; when nothing is configured **any publicly routable host is permitted** —
|
|
226
|
+
loopback, link-local, private and cloud-metadata addresses are always refused, but there is no
|
|
227
|
+
implicit allowlist beyond that.
|
|
228
|
+
|
|
137
229
|
Client configuration for an agent host:
|
|
138
230
|
|
|
139
231
|
```json
|