@ziprack/sdk 0.1.1 → 0.2.0

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.
Files changed (47) hide show
  1. package/dist/broadcast/index.d.ts +8 -1
  2. package/dist/core/mock/MockRack.d.ts +6 -0
  3. package/dist/core/types.d.ts +4 -0
  4. package/dist/docs/README.md +52 -0
  5. package/dist/docs/activity.md +140 -0
  6. package/dist/docs/ai.md +159 -0
  7. package/dist/docs/applet-packages.md +118 -0
  8. package/dist/docs/attempts.md +184 -0
  9. package/dist/docs/auth.md +106 -0
  10. package/dist/docs/broadcast.md +226 -0
  11. package/dist/docs/conformance.md +102 -0
  12. package/dist/docs/database.md +176 -0
  13. package/dist/docs/extending.md +256 -0
  14. package/dist/docs/installation.md +185 -0
  15. package/dist/docs/introduction.md +124 -0
  16. package/dist/docs/manifest.md +209 -0
  17. package/dist/docs/media.md +85 -0
  18. package/dist/docs/protocol.md +121 -0
  19. package/dist/docs/publishing.md +127 -0
  20. package/dist/docs/reporting.md +146 -0
  21. package/dist/docs/sdk.md +258 -0
  22. package/dist/docs/security.md +127 -0
  23. package/dist/docs/storage.md +131 -0
  24. package/dist/docs/studio.md +425 -0
  25. package/dist/docs/versioning.md +101 -0
  26. package/dist/docs/ziprack-1-authoring.md +182 -0
  27. package/dist/index.js +11 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/mcp/bin.d.ts +2 -0
  30. package/dist/mcp/bin.js +3 -0
  31. package/dist/mcp/docs.d.ts +31 -0
  32. package/dist/mcp/docs.js +107 -0
  33. package/dist/mcp/index.d.ts +4 -0
  34. package/dist/mcp/index.js +4 -0
  35. package/dist/mcp/rpc.d.ts +33 -0
  36. package/dist/mcp/rpc.js +86 -0
  37. package/dist/mcp/server.d.ts +23 -0
  38. package/dist/mcp/server.js +55 -0
  39. package/dist/mcp/tools.d.ts +20 -0
  40. package/dist/mcp/tools.js +106 -0
  41. package/dist/{store-DSV5q7C7.js → store-BXGon4iJ.js} +15 -5
  42. package/dist/store-BXGon4iJ.js.map +1 -0
  43. package/dist/testing.js +1 -1
  44. package/dist/ziprack-sdk.iife.js +1 -1
  45. package/dist/ziprack-sdk.iife.js.map +1 -1
  46. package/package.json +10 -4
  47. package/dist/store-DSV5q7C7.js.map +0 -1
@@ -0,0 +1,176 @@
1
+ # Database — `db@1`
2
+
3
+ - [Introduction](#introduction)
4
+ - [Declaring Collections](#declaring-collections)
5
+ - [Scopes](#scopes)
6
+ - [Reading and Writing](#reading-and-writing)
7
+ - [Queries](#queries)
8
+ - [Permissions](#permissions)
9
+ - [Quotas](#quotas)
10
+ - [How It Is Stored](#how-it-is-stored)
11
+
12
+ <a name="introduction"></a>
13
+ ## Introduction
14
+
15
+ `db@1` gives an applet CRUD over records in collections it declared, at a scope it declared. This is
16
+ working state: ideas on a board, a team roster, a saved layout, a shared catalog.
17
+
18
+ Records are mutable and arbitrarily shaped, which is exactly why **a JSON Schema is required, not
19
+ optional**. The schema is what the Rack validates writes against, what the admin renders and exports
20
+ records from, and what the installer derives indexes from. Without it, `db@1` would trade the
21
+ uniform export that makes [statements](activity.md) valuable for a pile of untyped JSON.
22
+
23
+ <a name="declaring-collections"></a>
24
+ ## Declaring Collections
25
+
26
+ ```json
27
+ "collections": [
28
+ {
29
+ "name": "ideas",
30
+ "scope": "room",
31
+ "schema": {
32
+ "type": "object",
33
+ "properties": {
34
+ "title": { "type": "string", "maxLength": 120 },
35
+ "body": { "type": "string", "maxLength": 2000 },
36
+ "status": { "type": "string", "enum": ["draft", "posted", "picked"] },
37
+ "votes": { "type": "integer", "minimum": 0 }
38
+ },
39
+ "required": ["title", "status"]
40
+ },
41
+ "indexed": ["status", "votes"],
42
+ "quota": { "max_records": 500, "max_bytes": 16384 },
43
+ "permissions": { "update": "owner", "delete": "facilitator" }
44
+ }
45
+ ]
46
+ ```
47
+
48
+ | Field | Rules |
49
+ |---|---|
50
+ | `name` | Required. `^[a-z][a-z0-9_]*$`, max 40, unique. |
51
+ | `scope` | Required. `attempt`, `user`, `room` or `applet`. |
52
+ | `schema` | Required. JSON Schema, `type: "object"`. |
53
+ | `indexed` | Optional, max 8 paths. Each must be a scalar property declared in the schema. Dotted paths allowed. |
54
+ | `quota` | Optional. `max_records`, `max_bytes` — ceilings you may lower, never raise. |
55
+ | `permissions` | Optional. `update` and `delete`: `owner` (default), `members`, `facilitator`. |
56
+
57
+ <a name="scopes"></a>
58
+ ## Scopes
59
+
60
+ The scope answers, at declaration time, the otherwise ambiguous question of who shares a record and
61
+ what happens to it when an attempt ends.
62
+
63
+ | Scope | Shared by | Lifetime | Requires |
64
+ |---|---|---|---|
65
+ | `attempt` | One attempt | **Frozen when the attempt completes**, alongside `state` | `attempt@1` |
66
+ | `user` | One participant | Outlives any attempt | — |
67
+ | `room` | Everyone in the room | The room | `broadcast@1` |
68
+ | `applet` | Everyone on this applet version | The version. Read-mostly: seed data, shared catalogs | — |
69
+
70
+ A session that has no attempt or no room cannot touch a collection scoped to one; the call is
71
+ refused with `forbidden` rather than silently falling back to something else.
72
+
73
+ <a name="reading-and-writing"></a>
74
+ ## Reading and Writing
75
+
76
+ ```ts
77
+ const ideas = Rack.db.collection<Idea>('ideas');
78
+
79
+ const created = await ideas.create({ title, status: 'draft', votes: 0 });
80
+ const idea = await ideas.get(created.data.record.id);
81
+
82
+ await ideas.update(id, { status: 'posted' }); // merges by default
83
+ await ideas.update(id, next, { merge: false }); // replaces the document
84
+ await ideas.delete(id);
85
+ ```
86
+
87
+ Every call resolves to an `AckResult`. A record comes back as:
88
+
89
+ ```ts
90
+ interface RecordRow<T> {
91
+ id: string; // uuid
92
+ data: T;
93
+ owner_id: number; // the participant who created it
94
+ created_at: string;
95
+ updated_at: string;
96
+ }
97
+ ```
98
+
99
+ Writes are validated against the declared schema **after** the merge, so a partial update that would
100
+ produce an invalid document is refused with `validation` and `details` naming the offending path.
101
+
102
+ <a name="queries"></a>
103
+ ## Queries
104
+
105
+ ```ts
106
+ const { data } = await ideas.list({
107
+ where: [['status', '=', 'posted'], ['votes', '>=', 3]],
108
+ orderBy: 'votes',
109
+ direction: 'desc',
110
+ limit: 50,
111
+ });
112
+
113
+ data.records; // RecordRow<Idea>[]
114
+ data.nextCursor; // pass back as { cursor } for the next page
115
+ ```
116
+
117
+ ```ts
118
+ const { data } = await ideas.count({ where: [['status', '=', 'draft']] });
119
+ ```
120
+
121
+ Operators: `=`, `!=`, `<`, `<=`, `>`, `>=`, `in`.
122
+
123
+ **Filtering and ordering work only on `indexed` paths.** That is not a limitation the SDK could hide
124
+ — it reflects how the data is actually stored (see below). Decide your query paths when you declare
125
+ the collection, and if you find yourself wanting a ninth index, the shape is probably wrong.
126
+
127
+ The page size caps at 100 (`ziprack-db.list_limit`). Lists are always scoped: you cannot read
128
+ another room's records, another participant's `user` records, or another attempt's records, because
129
+ the scope id is resolved from the session, never from your payload.
130
+
131
+ <a name="permissions"></a>
132
+ ## Permissions
133
+
134
+ Reads are scoped; writes are also owned. By default only the creator may update or delete a record.
135
+
136
+ | Value | Who may |
137
+ |---|---|
138
+ | `owner` | The participant who created it. The default. |
139
+ | `members` | Anyone who can see it — everyone in the room, for a `room` collection. |
140
+ | `facilitator` | The owner, plus whoever currently holds the facilitator role. |
141
+
142
+ Moderation of a shared board is `"delete": "facilitator"`. A genuinely collaborative document is
143
+ `"update": "members"`. Anything stricter than `owner` does not exist, because scope already decides
144
+ visibility.
145
+
146
+ <a name="quotas"></a>
147
+ ## Quotas
148
+
149
+ Declared quotas are enforced server-side regardless of what the applet believes:
150
+
151
+ | Limit | Default | Hard ceiling |
152
+ |---|---|---|
153
+ | `max_records` per scope | 500 | 5000 (`ziprack-db.max_records_per_scope`) |
154
+ | `max_bytes` per record | 16 KB | 64 KB (`ziprack-db.max_record_bytes`) |
155
+
156
+ Over quota, `create()` returns `quota_exceeded`. Handle it — a shared board hitting its cap during a
157
+ live session is a real event, not a theoretical one.
158
+
159
+ <a name="how-it-is-stored"></a>
160
+ ## How It Is Stored
161
+
162
+ Worth knowing, because it explains the constraints above.
163
+
164
+ Each collection gets its own MySQL table, created when the applet version installs. The document
165
+ lives in a JSON column; every path in `indexed` becomes a **generated column with an index on it**.
166
+ The declared schema is therefore also the index plan, which is what makes MySQL a good fit here
167
+ rather than a compromise — the queryable paths are known at install time, so there is no need for
168
+ containment queries over undeclared keys.
169
+
170
+ **Installing a version is a schema-bearing operation.** Adding a collection creates a table; adding
171
+ an indexed path adds a generated column; removing one drops it. Collections that no current manifest
172
+ declares any more are left in place until you run:
173
+
174
+ ```sh
175
+ php artisan ziprack:db:prune
176
+ ```
@@ -0,0 +1,256 @@
1
+ # Writing a Capability
2
+
3
+ - [When to Add One](#when-to-add-one)
4
+ - [The Capability Interface](#the-capability-interface)
5
+ - [Registering It](#registering-it)
6
+ - [Manifest Rules](#manifest-rules)
7
+ - [Message Handlers](#message-handlers)
8
+ - [Launch Payload](#launch-payload)
9
+ - [Install Hooks](#install-hooks)
10
+ - [Conformance and Status](#conformance-and-status)
11
+ - [The SDK Side](#the-sdk-side)
12
+
13
+ <a name="when-to-add-one"></a>
14
+ ## When to Add One
15
+
16
+ A capability is one applet-facing surface: a set of message types, a manifest section, a slice of the
17
+ launch payload, and the server-side service behind them. `ziprack/db` and `ziprack/ai` are both
18
+ exactly this shape.
19
+
20
+ Before adding one, apply the framework's own test:
21
+
22
+ > If a capability requires an applet author to reason about device identity, transport, or
23
+ > deployment topology, it does not belong in zipRACK.
24
+
25
+ And check it is not already expressible. A great many ideas reduce to `db@1` with a different
26
+ collection, or to `broadcast@1` with a different event name. The value of seven surfaces is that
27
+ there are seven.
28
+
29
+ <a name="the-capability-interface"></a>
30
+ ## The Capability Interface
31
+
32
+ One package, one `Capability`. Extend `AbstractCapability` and override only what you need — every
33
+ method has a sensible no-op default.
34
+
35
+ ```php
36
+ namespace ZipRack\Survey;
37
+
38
+ use ZipRack\Core\Capabilities\AbstractCapability;
39
+
40
+ class SurveyCapability extends AbstractCapability
41
+ {
42
+ public function id(): string
43
+ {
44
+ return 'survey'; // ^[a-z][a-z0-9_]*$
45
+ }
46
+
47
+ public function versions(): array
48
+ {
49
+ return [1]; // may implement several at once: [1, 2]
50
+ }
51
+ }
52
+ ```
53
+
54
+ | Method | Purpose |
55
+ |---|---|
56
+ | `id()` | The name in manifests and message types: `survey`, `survey:submit`. |
57
+ | `versions()` | Capability versions this package implements. Metadata, independent of package semver. |
58
+ | `manifestSections()` | Top-level manifest keys this capability owns. |
59
+ | `manifestRules()` | Laravel rules for those sections. |
60
+ | `validateManifest()` | Cross-field checks; add errors to the validator. |
61
+ | `messageHandlers()` | `type => handler class`. |
62
+ | `launchPayload()` | What to merge into `rack:launch` under this capability's id. |
63
+ | `onInstall()` / `onPromote()` / `onRetire()` | Version lifecycle hooks. `onInstall()` runs inside the install transaction. |
64
+ | `adminRoutes()` | Headless admin routes, registered only when `ziprack/admin` is installed. |
65
+ | `conformanceTests()` | Checks contributed to the conformance suite. |
66
+ | `statusChecks()` | Warnings for `ziprack:status` and `/ziprack/health`. |
67
+
68
+ <a name="registering-it"></a>
69
+ ## Registering It
70
+
71
+ ```php
72
+ class SurveyServiceProvider extends ServiceProvider
73
+ {
74
+ public function register(): void
75
+ {
76
+ $this->mergeConfigFrom(__DIR__.'/../config/ziprack-survey.php', 'ziprack-survey');
77
+ }
78
+
79
+ public function boot(): void
80
+ {
81
+ $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
82
+
83
+ ZipRack::register($this->app->make(SurveyCapability::class));
84
+
85
+ if ($this->app->runningInConsole()) {
86
+ $this->publishes([
87
+ __DIR__.'/../config/ziprack-survey.php' => config_path('ziprack-survey.php'),
88
+ ], 'ziprack-config');
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ Migrations load; they are never published. Config publishes; it is meant to diverge. That split is
95
+ [not negotiable](installation.md#publishable-and-non-publishable) — it is what keeps the fleet
96
+ uniform.
97
+
98
+ The registry is built from installed providers, so installing the package *is* declaring the
99
+ capability. There is no separate list to fall out of sync.
100
+
101
+ <a name="manifest-rules"></a>
102
+ ## Manifest Rules
103
+
104
+ ```php
105
+ public function manifestSections(): array
106
+ {
107
+ return ['surveys'];
108
+ }
109
+
110
+ public function manifestRules(): array
111
+ {
112
+ return [
113
+ 'surveys' => ['required', 'array', 'min:1'],
114
+ 'surveys.*.id' => ['required', 'string', 'max:100', 'regex:/^[a-z][a-z0-9_.-]*$/'],
115
+ 'surveys.*.questions' => ['required', 'array', 'min:1'],
116
+ ];
117
+ }
118
+
119
+ public function validateManifest(array $data, Validator $validator): void
120
+ {
121
+ $ids = [];
122
+ foreach ($data['surveys'] ?? [] as $index => $survey) {
123
+ if (isset($ids[$survey['id'] ?? null])) {
124
+ $validator->errors()->add("surveys.{$index}.id", "Duplicate survey id [{$survey['id']}].");
125
+ }
126
+ $ids[$survey['id'] ?? ''] = true;
127
+ }
128
+ }
129
+ ```
130
+
131
+ The core validator enforces the section/capability relationship for you: a manifest containing
132
+ `surveys` without declaring `survey@1` fails, and so does one declaring a section no installed
133
+ capability owns.
134
+
135
+ Three conventions worth keeping: uniqueness errors name the duplicate, a section that depends on
136
+ another capability says so explicitly (`"A room-scoped X requires the [broadcast] capability"`), and
137
+ anything with a quota takes its ceiling from config rather than hard-coding it.
138
+
139
+ <a name="message-handlers"></a>
140
+ ## Message Handlers
141
+
142
+ ```php
143
+ public function messageHandlers(): array
144
+ {
145
+ return ['survey:submit' => SubmitSurvey::class];
146
+ }
147
+ ```
148
+
149
+ ```php
150
+ class SubmitSurvey implements MessageHandler
151
+ {
152
+ public function rules(MessageContext $context): array
153
+ {
154
+ return [
155
+ 'survey' => ['required', 'string', 'max:100'],
156
+ 'answers' => ['required', 'array'],
157
+ ];
158
+ }
159
+
160
+ public function handle(MessageContext $context, array $payload): Ack
161
+ {
162
+ $survey = SurveyManifest::of($context->manifest())->find($payload['survey'])
163
+ ?? throw MessageException::notFound("Survey [{$payload['survey']}] is not declared.");
164
+
165
+ // $context->participant, $context->version, $context->attribute('attempt_id'|'room_id')
166
+
167
+ return Ack::ok(['id' => $id]);
168
+ }
169
+ }
170
+ ```
171
+
172
+ The dispatcher has already checked that the manifest declares your capability at a version this Rack
173
+ implements, and has already validated the payload against `rules()` — a handler never sees an
174
+ invalid payload or an undeclared call.
175
+
176
+ Throw `MessageException::forbidden() / notFound() / quota() / completed() / conflict()` for expected
177
+ failures; anything else is reported and acks `persist_failed`. Never leak an internal message into
178
+ an ack.
179
+
180
+ Message types must match `^[a-z][a-z0-9_]*:[a-z][a-z0-9_.]*$` and are namespaced by your capability
181
+ id. Everything the applet may influence about scope comes from `MessageContext`, never from the
182
+ payload — that is how a room-scoped write cannot address another room.
183
+
184
+ <a name="launch-payload"></a>
185
+ ## Launch Payload
186
+
187
+ ```php
188
+ public function launchPayload(LaunchContext $context): ?array
189
+ {
190
+ return ['surveys' => SurveyManifest::of($context->manifest())->summaries()];
191
+ }
192
+ ```
193
+
194
+ Merged under your capability id, so the applet reads `launch.survey.surveys`. Return `null` to
195
+ contribute nothing. Use `$context->remember(key, fn)` to share a resolved model with your message
196
+ handlers rather than querying twice.
197
+
198
+ Send what the applet needs to render its first frame, and nothing more. Launch data crosses into an
199
+ untrusted frame: it is the one place where over-sharing is silent.
200
+
201
+ <a name="install-hooks"></a>
202
+ ## Install Hooks
203
+
204
+ ```php
205
+ public function onInstall(AppletVersion $version, ?AppletVersion $previous): void
206
+ {
207
+ $this->installer->install($version, $previous); // schema-bearing work belongs here
208
+ }
209
+ ```
210
+
211
+ `onInstall()` runs inside the install transaction, with the previous current version for comparison
212
+ — which is what lets `db@1` create, alter and drop generated columns as collections change between
213
+ versions. If it throws, the whole installation rolls back and extracted files are removed.
214
+
215
+ <a name="conformance-and-status"></a>
216
+ ## Conformance and Status
217
+
218
+ ```php
219
+ public function conformanceTests(): array
220
+ {
221
+ return [SurveyAnswersAreImmutable::class];
222
+ }
223
+
224
+ public function statusChecks(): array
225
+ {
226
+ return $this->configured() ? [] : [[
227
+ 'level' => 'warning',
228
+ 'message' => 'survey@1 has no store configured; submissions are discarded.',
229
+ ]];
230
+ }
231
+ ```
232
+
233
+ A conformance check is a class with `name()` and `run(Fixture $fixture)`, failing by throwing. Write
234
+ one per invariant the capability promises an applet author — not one per method.
235
+
236
+ <a name="the-sdk-side"></a>
237
+ ## The SDK Side
238
+
239
+ A capability is only half-built until `@ziprack/sdk` can call it. Add a module under
240
+ `sdk/src/{capability}/`, take a `Link` in the constructor, and expose it on `RackClient`:
241
+
242
+ ```ts
243
+ export class SurveyApi {
244
+ public constructor(private readonly link: Link) {}
245
+
246
+ public submit(survey: string, answers: JsonObject): Promise<AckResult<{ id: string }>> {
247
+ return this.link.send('survey:submit', { survey, answers });
248
+ }
249
+ }
250
+ ```
251
+
252
+ Then implement it in `MockRack` as well. **A capability with no mock is a capability applet authors
253
+ cannot develop against**, and standalone development is the property the framework most depends on —
254
+ losing it costs more than the capability is worth.
255
+
256
+ Bump the capability version only when this surface changes. Refactors below it are free.
@@ -0,0 +1,185 @@
1
+ # Installation
2
+
3
+ - [Composing a Rack](#composing-a-rack)
4
+ - [The Deployment Skeleton](#the-deployment-skeleton)
5
+ - [Configuration](#configuration)
6
+ - [Routes](#routes)
7
+ - [Participants and Guests](#participants-and-guests)
8
+ - [Publishable and Non-Publishable](#publishable-and-non-publishable)
9
+ - [Console Commands](#console-commands)
10
+
11
+ <a name="composing-a-rack"></a>
12
+ ## Composing a Rack
13
+
14
+ **A deployment's `composer.json` is its capability declaration.** The capability registry is built
15
+ by iterating installed service providers, so it cannot disagree with what is actually installed.
16
+
17
+ ```json
18
+ "require": {
19
+ "ziprack/core": "^1.0",
20
+ "ziprack/auth": "^1.0",
21
+ "ziprack/attempt": "^1.0",
22
+ "ziprack/activity": "^1.0",
23
+ "ziprack/db": "^1.0",
24
+ "ziprack/ai": "^1.0",
25
+ "ziprack/broadcast": "^1.0",
26
+ "ziprack/storage": "^1.0",
27
+ "ziprack/admin": "^1.0"
28
+ },
29
+ "require-dev": {
30
+ "ziprack/conformance": "^1.0"
31
+ }
32
+ ```
33
+
34
+ Remove `ziprack/db` and an applet requesting `db@1` is refused at upload with a message naming what
35
+ this Rack does implement. Drop `ziprack/broadcast` and there is no Reverb dependency, no WebSocket
36
+ port and no room tables — a smaller attack surface, not just a smaller install.
37
+
38
+ `ziprack/core` is mandatory; every capability package requires it. `ziprack/admin` is optional and
39
+ headless: it ships JSON endpoints and export services, never pages. `ziprack/conformance` is a dev
40
+ dependency that belongs in CI.
41
+
42
+ Packages resolve from private VCS repositories:
43
+
44
+ ```json
45
+ "repositories": [
46
+ { "type": "vcs", "url": "git@github.com:Collective-Next/ziprack-core.git" },
47
+ { "type": "vcs", "url": "git@github.com:Collective-Next/ziprack-attempt.git" }
48
+ ]
49
+ ```
50
+
51
+ Inside the monorepo they resolve from `../packages/*` path repositories instead, so changes are
52
+ testable without tagging.
53
+
54
+ <a name="the-deployment-skeleton"></a>
55
+ ## The Deployment Skeleton
56
+
57
+ `skeleton/` is a Laravel application template cloned once per engagement and then owned by that
58
+ engagement. It holds what is *meant* to diverge: branding, environment, the deployment pipeline, the
59
+ admin UI, and the capability list.
60
+
61
+ ```sh
62
+ cp .env.example .env && php artisan key:generate
63
+ php artisan migrate --seed
64
+ npm install && npm run build
65
+ php artisan ziprack:install path/to/applet-1.0.0.zip --activate
66
+ php artisan ziprack:conformance
67
+ ```
68
+
69
+ Migrations ship inside the packages and load through `loadMigrationsFrom()`; they are never
70
+ published, because schema must be identical on every Rack.
71
+
72
+ <a name="configuration"></a>
73
+ ## Configuration
74
+
75
+ Each package publishes one config file. The defaults are production-safe; the values below are the
76
+ ones you will actually touch.
77
+
78
+ ### `config/ziprack.php`
79
+
80
+ | Key | Default | Notes |
81
+ |---|---|---|
82
+ | `disk` | `local` | Where packages are extracted. Must not be publicly served. |
83
+ | `max_upload_mb` | `50` | Package size cap. |
84
+ | `allowed_extensions` | static list | Anything else rejects the whole upload. |
85
+ | `asset_token_ttl_hours` | `6` | Lifetime of the signed package-asset token. |
86
+ | `session_ttl_hours` | `6` | Lifetime of a launch session. |
87
+ | `csp.img_hosts`, `csp.media_hosts` | `[]` | Per-deployment allowlist. A bare scheme is rejected. |
88
+ | `media.providers` | `['vimeo','youtube']` | Drives `frame-src` and manifest validation. |
89
+ | `publishing.require_sbom` | `true` | Refuse packages with no SBOM. |
90
+ | `publishing.require_review_for_new_capabilities` | `true` | Block promotion until a widened `requires` is approved. |
91
+ | `publishing.osv_lookup` | `true` | Batch the SBOM's purls against OSV.dev after install. |
92
+ | `audit.enabled` | `true` | Per-applet audit log of capability calls. |
93
+ | `health_token` | `null` | Bearer token for `/ziprack/health`. |
94
+
95
+ ### Capability configs
96
+
97
+ | File | Notable keys |
98
+ |---|---|
99
+ | `ziprack-auth.php` | `releasable` — the single place a deployment answers "what can applets see about our people"; `claim_map` |
100
+ | `ziprack-attempt.php` | `max_state_bytes` (64 KB), `max_segment_keys` (20) |
101
+ | `ziprack-db.php` | `max_records_per_scope` (5000), `max_record_bytes` (64 KB), `max_indexed_paths` (8), `list_limit` (100) |
102
+ | `ziprack-storage.php` | `disk`, `max_bytes_cap` (50 MB), `url_ttl_minutes` (10), `scanner` (`null` \| `clamav`) |
103
+ | `ziprack-ai.php` | `provider`, `models.analysis`, `models.chat`, `allowed_models`, the whole `budget` block |
104
+ | `ziprack-broadcast.php` | `join_prefix`, `facilitator_grace_seconds` (60), `presence_window_seconds` (45), `max_event_bytes` |
105
+ | `ziprack-admin.php` | `prefix` (`ziprack/admin`), `middleware` (`web`, `auth`, `can:ziprack.admin`) |
106
+
107
+ Ceilings in these files are **hard**: a manifest asking for a larger quota or budget fails
108
+ validation. Defaults apply when a manifest declares nothing.
109
+
110
+ <a name="routes"></a>
111
+ ## Routes
112
+
113
+ | Route | Purpose |
114
+ |---|---|
115
+ | `GET /a/{applet}` | The player: chrome plus the sandboxed iframe. Middleware from `ziprack.routes.middleware.player`, default `['web','auth']`. |
116
+ | `GET /ziprack/pkg/{token}/{path}` | Package assets, streamed with the contract's headers and CSP. Token-authenticated, stateless. |
117
+ | `GET /ziprack/assets/{file}` | The shell bundles (`shell.js`, `shell-broadcast.js`, `sdk.js`). |
118
+ | `POST /ziprack/s/{session}/messages` | The one door every capability call goes through. Batched, max 50 per request. |
119
+ | `GET /ziprack/s/{session}/stream/{requestId}` | Server-sent events for streamed acks (`ai:chat`). |
120
+ | `POST /ziprack/s/{session}/files` | Multipart upload target for `storage:put` (`storage`). |
121
+ | `GET /ziprack/f/{file}` | Signed, short-lived download of a stored file (`storage`). |
122
+ | `POST /ziprack/s/{session}/broadcast/auth` | Rack-side WebSocket channel authentication (`broadcast`). |
123
+ | `GET /ziprack/health` | Fleet visibility: format, core version, capabilities, warnings. |
124
+ | `GET /r/{code}` | Join a room by code (`broadcast`). |
125
+ | `*` `/ziprack/admin/*` | The headless admin API (`ziprack/admin`). |
126
+
127
+ Session routes are deliberately stateless: token in the path, no session cookie, no CSRF, rate
128
+ limited by `ziprack.routes.session_rate_limit`.
129
+
130
+ <a name="participants-and-guests"></a>
131
+ ## Participants and Guests
132
+
133
+ By default the player sits behind `auth` and each authenticated user maps to a `Participant`.
134
+ Light group activities often have no identity source, so a Rack may enable guests:
135
+
136
+ ```
137
+ ZIPRACK_GUESTS=true
138
+ ```
139
+
140
+ and drop `auth` from the player middleware. An unauthenticated visitor then gets a cookie-keyed
141
+ participant, which is enough for `opt_in` and `first_in` rooms. `roster` rooms and any applet
142
+ declaring `auth@1` still need real users.
143
+
144
+ To resolve participants differently — SSO subject, an LTI launch, an external roster — bind
145
+ `ZipRack\Core\Contracts\ParticipantResolver` in a service provider.
146
+
147
+ <a name="publishable-and-non-publishable"></a>
148
+ ## Publishable and Non-Publishable
149
+
150
+ Every publishable file is a place the fleet can drift, so the split is deliberate.
151
+
152
+ **Publishable — meant to diverge:** config files, the player chrome and holding views
153
+ (`resources/views/vendor/ziprack`), translations, notification templates.
154
+
155
+ **Never publishable — these are the contract:**
156
+
157
+ | Artifact | Why |
158
+ |---|---|
159
+ | Migrations | Schema must be identical everywhere. |
160
+ | `ManifestValidator` | An applet must validate the same way on every Rack, or portability is fiction. |
161
+ | `PackageInstaller` file rules | Traversal, symlink and extension checks are security code. |
162
+ | Bridge and envelope handling | The `event.source` trust check is the whole boundary. |
163
+ | `ContentSecurityPolicy` | Directives are fixed; only the host allowlist is configurable. |
164
+ | `CapabilityRegistry` and dispatch | The runtime rejection of undeclared calls. |
165
+
166
+ Where deployments genuinely need to vary, the variation is configuration the core reads — not code
167
+ the deployment owns. A Rack can add an approved image host; it cannot reintroduce `https:`.
168
+
169
+ <a name="console-commands"></a>
170
+ ## Console Commands
171
+
172
+ | Command | Does |
173
+ |---|---|
174
+ | `ziprack:install {zip} [--applet=] [--activate]` | Install a package as a new version. |
175
+ | `ziprack:promote {slug} {version} [--force]` | Make a version current. `--force` also approves a pending capability review. |
176
+ | `ziprack:retire {slug} {version}` | Kill switch: the version can no longer be launched. |
177
+ | `ziprack:status [--json]` | Format, core version, installed capabilities, warnings. |
178
+ | `ziprack:conformance [--json]` | Verify this Rack honours `ziprack/1`. |
179
+ | `ziprack:conformance:install` | Publish a test that runs the suite inside the deployment's own Pest run. |
180
+ | `ziprack:profile:set {email} {key} {value}` | Set one profile attribute. |
181
+ | `ziprack:roster:import {csv}` | Import profile attributes by email. |
182
+ | `ziprack:room:create {slug} [--name=] [--mode=] [--roster=]` | Create a room and print its join code. |
183
+ | `ziprack:rooms:tick` | Vacate and reassign dropped facilitators. **Schedule every minute.** |
184
+ | `ziprack:db:prune [--force]` | Drop tables of collections no current manifest declares. |
185
+ | `ziprack:response-analysis [--applet=] [--force]` | Regenerate admin AI summaries. **Schedule every few hours.** |