@ziprack/sdk 0.2.0 → 0.3.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.
- package/dist/auth/index.d.ts +11 -0
- package/dist/core/mock/MockRack.d.ts +15 -0
- package/dist/core/rack.d.ts +9 -0
- package/dist/core/types.d.ts +7 -0
- package/dist/db/index.d.ts +2 -1
- package/dist/docs/README.md +7 -3
- package/dist/docs/activity.md +4 -0
- package/dist/docs/applet-packages.md +18 -8
- package/dist/docs/attempts.md +6 -3
- package/dist/docs/auth.md +76 -0
- package/dist/docs/broadcast.md +1 -1
- package/dist/docs/conformance.md +14 -13
- package/dist/docs/database.md +308 -11
- package/dist/docs/extending.md +76 -5
- package/dist/docs/installation.md +11 -5
- package/dist/docs/introduction.md +7 -5
- package/dist/docs/manifest.md +34 -1
- package/dist/docs/mcp.md +202 -0
- package/dist/docs/protocol.md +42 -0
- package/dist/docs/publishing.md +87 -8
- package/dist/docs/reporting.md +55 -4
- package/dist/docs/sdk.md +28 -4
- package/dist/docs/security.md +87 -8
- package/dist/docs/storage.md +1 -1
- package/dist/docs/studio.md +1 -1
- package/dist/docs/versioning.md +16 -0
- package/dist/docs/ziprack-1-authoring.md +45 -9
- package/dist/index.js +41 -18
- package/dist/index.js.map +1 -1
- package/dist/{store-BXGon4iJ.js → store-DQbG_t2D.js} +15 -6
- package/dist/store-DQbG_t2D.js.map +1 -0
- package/dist/testing.js +1 -1
- package/dist/ziprack-sdk.iife.js +1 -1
- package/dist/ziprack-sdk.iife.js.map +1 -1
- package/package.json +1 -1
- package/dist/store-BXGon4iJ.js.map +0 -1
package/dist/docs/database.md
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
- [Queries](#queries)
|
|
8
8
|
- [Permissions](#permissions)
|
|
9
9
|
- [Quotas](#quotas)
|
|
10
|
+
- [Seeding](#seeding)
|
|
11
|
+
- [Data Across Versions](#data-across-versions)
|
|
12
|
+
- [Data Migrations](#data-migrations)
|
|
13
|
+
- [Snapshots](#snapshots)
|
|
10
14
|
- [How It Is Stored](#how-it-is-stored)
|
|
15
|
+
- [Rack Side](#rack-side)
|
|
11
16
|
|
|
12
17
|
<a name="introduction"></a>
|
|
13
18
|
## Introduction
|
|
@@ -53,6 +58,7 @@ uniform export that makes [statements](activity.md) valuable for a pile of untyp
|
|
|
53
58
|
| `indexed` | Optional, max 8 paths. Each must be a scalar property declared in the schema. Dotted paths allowed. |
|
|
54
59
|
| `quota` | Optional. `max_records`, `max_bytes` — ceilings you may lower, never raise. |
|
|
55
60
|
| `permissions` | Optional. `update` and `delete`: `owner` (default), `members`, `facilitator`. |
|
|
61
|
+
| `seed` | Optional, `applet` scope only. Path to a JSON file in the package. See [Seeding](#seeding). |
|
|
56
62
|
|
|
57
63
|
<a name="scopes"></a>
|
|
58
64
|
## Scopes
|
|
@@ -65,11 +71,19 @@ what happens to it when an attempt ends.
|
|
|
65
71
|
| `attempt` | One attempt | **Frozen when the attempt completes**, alongside `state` | `attempt@1` |
|
|
66
72
|
| `user` | One participant | Outlives any attempt | — |
|
|
67
73
|
| `room` | Everyone in the room | The room | `broadcast@1` |
|
|
68
|
-
| `applet` | Everyone
|
|
74
|
+
| `applet` | Everyone using the applet, on every version | The applet. Carries over every promotion. Seed data, shared catalogs | — |
|
|
69
75
|
|
|
70
76
|
A session that has no attempt or no room cannot touch a collection scoped to one; the call is
|
|
71
77
|
refused with `forbidden` rather than silently falling back to something else.
|
|
72
78
|
|
|
79
|
+
`user`, `room` and `applet` records belong to the applet, not to a version, so they are still there
|
|
80
|
+
after you promote a new one — see [Data Across Versions](#data-across-versions). `attempt` records
|
|
81
|
+
stay with their attempt, and so with the version it ran on.
|
|
82
|
+
|
|
83
|
+
A session reads and writes a collection through the definition of the version it launched on. An
|
|
84
|
+
attempt pinned to an earlier version keeps that version's schema, indexes and quota until it
|
|
85
|
+
completes, even after a later version changed them.
|
|
86
|
+
|
|
73
87
|
<a name="reading-and-writing"></a>
|
|
74
88
|
## Reading and Writing
|
|
75
89
|
|
|
@@ -90,7 +104,7 @@ Every call resolves to an `AckResult`. A record comes back as:
|
|
|
90
104
|
interface RecordRow<T> {
|
|
91
105
|
id: string; // uuid
|
|
92
106
|
data: T;
|
|
93
|
-
owner_id: number;
|
|
107
|
+
owner_id: number | null; // the participant who created it; null when seeded
|
|
94
108
|
created_at: string;
|
|
95
109
|
updated_at: string;
|
|
96
110
|
}
|
|
@@ -156,21 +170,304 @@ Declared quotas are enforced server-side regardless of what the applet believes:
|
|
|
156
170
|
Over quota, `create()` returns `quota_exceeded`. Handle it — a shared board hitting its cap during a
|
|
157
171
|
live session is a real event, not a theoretical one.
|
|
158
172
|
|
|
173
|
+
<a name="seeding"></a>
|
|
174
|
+
## Seeding
|
|
175
|
+
|
|
176
|
+
An `applet` collection can ship its initial records in the package. This is how you bring along a
|
|
177
|
+
catalog, a question bank or any other data an existing app kept in a JSON file:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"name": "products",
|
|
182
|
+
"scope": "applet",
|
|
183
|
+
"schema": { "type": "object", "properties": { "sku": { "type": "string" }, "title": { "type": "string" } } },
|
|
184
|
+
"indexed": ["sku"],
|
|
185
|
+
"seed": "data/products.json"
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The seed file is a JSON array of records, each one a document exactly as `create()` would take it:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
[
|
|
193
|
+
{ "sku": "A-100", "title": "Starter kit" },
|
|
194
|
+
{ "sku": "A-200", "title": "Refill pack" }
|
|
195
|
+
]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The seed is **checked at install and loaded at promotion**. At install the Rack holds every record
|
|
199
|
+
to the rules a write would face: it must be an object, it must match the schema, it must fit
|
|
200
|
+
`max_bytes`, and the file must fit `max_records`. One bad record refuses the whole package, and the
|
|
201
|
+
error names the record's index and the failing path. A package with a bad seed never installs.
|
|
202
|
+
|
|
203
|
+
When the version is promoted, the seed loads into the collection **only if its live dataset is
|
|
204
|
+
empty**. A seed is the starting data, never a reset: once the applet has records, a later version's
|
|
205
|
+
seed file is not loaded, even if it changed. The promotion plan says so. To change records that
|
|
206
|
+
already exist, ship a [data migration](#data-migrations).
|
|
207
|
+
|
|
208
|
+
Things to know:
|
|
209
|
+
|
|
210
|
+
- **Only `applet` scope.** No room, participant or attempt exists at install, so nothing else has
|
|
211
|
+
a scope to seed into.
|
|
212
|
+
- **Loaded once, then the data is yours.** Records carry over every promotion. If the collection is
|
|
213
|
+
emptied (preview records do not count), the next promotion loads the seed again.
|
|
214
|
+
- **Seeded records have no owner.** `owner_id` is `null`, so under the default `owner` permission
|
|
215
|
+
participants can read them but cannot change them. Declare `members` or `facilitator` if they
|
|
216
|
+
should be editable.
|
|
217
|
+
- **No `RecordChanged` events.** The seed loads inside the promotion, before any session runs the
|
|
218
|
+
new version.
|
|
219
|
+
- **The quota still applies.** A seed is capped at 5000 records (`max_records_per_scope`), and so
|
|
220
|
+
is everything later written to that scope.
|
|
221
|
+
- **Not in the mock Rack.** The SDK's `MockRack` does not load seed files. In local development,
|
|
222
|
+
create the records from your dev harness.
|
|
223
|
+
|
|
224
|
+
<a name="data-across-versions"></a>
|
|
225
|
+
## Data Across Versions
|
|
226
|
+
|
|
227
|
+
Promoting a version carries the applet's data over to it. `user`, `room` and `applet` records are
|
|
228
|
+
kept and checked against the new version's collections; `attempt` records are left alone, in the
|
|
229
|
+
shape their attempt wrote them. Nothing about your data changes at install: a new version's
|
|
230
|
+
collections are checked, and names the applet has never had are registered empty, but the live
|
|
231
|
+
version keeps reading what it always read until promotion.
|
|
232
|
+
|
|
233
|
+
At promotion the Rack plans the cutover. For each declared collection it works out whether it is
|
|
234
|
+
`added`, `removed`, `restored`, `changed` or `unchanged`, describes the structural diff (a new
|
|
235
|
+
required property, a narrowed enum, a changed quota), and — when no migration runs — validates
|
|
236
|
+
every existing record against the new schema and quota. A promotion that would leave records the
|
|
237
|
+
new version rejects, strand a collection that holds records, or roll migrations back is not done
|
|
238
|
+
silently: it is refused until someone confirms, and the refusal carries the plan. See
|
|
239
|
+
[Promotion](publishing.md#promotion) for how that looks from the admin API, the CLI and MCP.
|
|
240
|
+
|
|
241
|
+
| The new version… | What happens at promotion |
|
|
242
|
+
|---|---|
|
|
243
|
+
| Changes nothing about its collections | Promotes. Records are untouched; no snapshot is taken. |
|
|
244
|
+
| Makes a compatible change: a new optional property, a wider enum, a new index, a new collection | Promotes. Existing records validate, so they carry over as they are. A snapshot is taken first when a collection holding records changed. New collections start empty, or with their seed. |
|
|
245
|
+
| Ships a [migration](#data-migrations) that has not run on this applet | Must be promoted from the Rack's UI, which runs the migrations in the browser, checks the result, then commits it. The CLI and MCP refuse it (`migration_required`). |
|
|
246
|
+
| Makes an incompatible change with no migration | Refused with `data_loss`, naming how many records the new schema rejects, with samples. Once confirmed, rejected records are deleted (`invalid: discard`, the default) or kept as they are (`keep`). A kept record fails validation on its next update until it is fixed. |
|
|
247
|
+
| Drops a collection that holds records | Refused with `data_loss`. Once confirmed, the collection is set aside with its records; `ziprack:db:prune` deletes them. A later version that declares it again gets them back. |
|
|
248
|
+
| Changes a collection's `scope` | Refused **at install**: `Collection [x] cannot change scope from [room] to [applet]; use a new collection name.` A migration cannot move records between scopes either. |
|
|
249
|
+
| Lowers `max_records` below what a scope holds, or `max_bytes` below a record's size | Refused with `data_loss`. Once confirmed, oversize records count as rejected; a scope over its new record quota keeps its records but refuses `create()` with `quota_exceeded` until it is back under. |
|
|
250
|
+
| Ships a changed seed file for a collection that has records | Promotes. The seed is not loaded, and the plan notes it. Use a migration. |
|
|
251
|
+
| Is an earlier version that lacks migrations already run (a rollback) | Those migrations run `down`, newest first, from the source the Rack stored when they ran up — in the browser, like any migration, and flagged as a rollback. A migration with no `down` stops it; restore the [snapshot](#snapshots) taken before it ran instead. An earlier version with no migrations involved is treated like any other change. |
|
|
252
|
+
| Is the applet's first | Nothing to carry over. Its migrations are recorded as applied without running, and the seed loads. |
|
|
253
|
+
|
|
254
|
+
Uploading without activating changes nothing about the data; the plan is made when someone
|
|
255
|
+
promotes. An upload *with* activate tries the promotion, and if the cutover needs a decision the
|
|
256
|
+
version stays installed but not current.
|
|
257
|
+
|
|
258
|
+
Two more rules apply to every cutover:
|
|
259
|
+
|
|
260
|
+
- **Preview records are dropped where shapes change.** A cutover that runs migrations deletes every
|
|
261
|
+
[preview](attempts.md#preview-runs) record of the applet; one that does not deletes the preview
|
|
262
|
+
records of the collections whose definition changed. Migrations never see preview records.
|
|
263
|
+
- **Pinned attempts keep the old shape.** An in-progress attempt on the outgoing version reads and
|
|
264
|
+
writes `user`, `room` and `applet` records through that version's definition until it completes.
|
|
265
|
+
After a cutover that reshapes them, it can still write records in the old shape. If your applet is
|
|
266
|
+
pinned, write code that tolerates both, or reshape only what in-progress attempts do not write.
|
|
267
|
+
|
|
268
|
+
<a name="data-migrations"></a>
|
|
269
|
+
## Data Migrations
|
|
270
|
+
|
|
271
|
+
When a new version needs existing records in a new shape — a renamed property, a split collection,
|
|
272
|
+
a value back-filled from another — ship a data migration. A migration is an ES module in a
|
|
273
|
+
`migrations/` directory at the zip root:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
my-applet-2.0.0.zip
|
|
277
|
+
├── manifest.json
|
|
278
|
+
├── migrations/
|
|
279
|
+
│ ├── 2026_09_01_090000_add_difficulty.js
|
|
280
|
+
│ └── 2026_10_09_120000_split_archived_ideas.js
|
|
281
|
+
└── …
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
| Rule | |
|
|
285
|
+
|---|---|
|
|
286
|
+
| Name | `YYYY_MM_DD_HHMMSS_name.js` (or `.mjs`), lowercase, directly inside `migrations/`. Anything else refuses the package at install. |
|
|
287
|
+
| Order | Name order. The timestamp prefix is the order. |
|
|
288
|
+
| Cumulative | **Keep every migration you have ever shipped.** The Rack runs the ones this applet has not run yet, so any earlier version can be brought forward. |
|
|
289
|
+
| Self-contained | Each file is imported on its own, with no network. Do not import other files or packages. The starter copies `migrations/` into the zip as written, unbundled. |
|
|
290
|
+
| Exports | `up(db, ctx)`, and `down(db, ctx)` if the change can be undone. Either as named exports or on the default export. |
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
// migrations/2026_09_01_090000_add_difficulty.js
|
|
294
|
+
export function up(db, ctx) {
|
|
295
|
+
for (const question of db.questions) {
|
|
296
|
+
question.data.difficulty ??= 'medium';
|
|
297
|
+
}
|
|
298
|
+
ctx.log(`set a difficulty on ${db.questions.length} questions`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function down(db) {
|
|
302
|
+
for (const question of db.questions) {
|
|
303
|
+
delete question.data.difficulty;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`db` holds the applet's carried records, by collection name: every `user`, `room` and `applet`
|
|
309
|
+
collection, including ones the outgoing version no longer declares. A record is:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
interface MigrationRecord {
|
|
313
|
+
id: string;
|
|
314
|
+
scope_id: number; // the room or participant; 0 for applet scope
|
|
315
|
+
owner_id: number | null;
|
|
316
|
+
data: object; // the document, as create() took it
|
|
317
|
+
created_at: string;
|
|
318
|
+
updated_at: string;
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Mutate it in place, or return `{ collection: records[] }` for the collections you replace. A
|
|
323
|
+
collection you return replaces that collection; one you leave out is kept unchanged, so its records
|
|
324
|
+
must already fit the target version.
|
|
325
|
+
|
|
326
|
+
```js
|
|
327
|
+
// migrations/2026_10_09_120000_split_archived_ideas.js
|
|
328
|
+
export default {
|
|
329
|
+
up(db) {
|
|
330
|
+
return {
|
|
331
|
+
ideas: db.ideas.filter((idea) => idea.data.status !== 'archived'),
|
|
332
|
+
archived_ideas: db.ideas.filter((idea) => idea.data.status === 'archived'),
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
down(db) {
|
|
337
|
+
return { ideas: [...db.ideas, ...db.archived_ideas] };
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
A record that appears under another collection's name moves there, keeping its id. To add a record,
|
|
343
|
+
leave out `id`; it is given a new one. A collection the target version does not declare cannot be
|
|
344
|
+
written to: returning it is ignored.
|
|
345
|
+
|
|
346
|
+
`ctx` is `{ direction, applet, from, to, scopes, log }`: `'up'` or `'down'`, the applet slug, the
|
|
347
|
+
outgoing and target version strings (`from` is `null` when nothing was current), each collection's
|
|
348
|
+
scope by name, and `log(...)` for lines kept with the snapshot. `console.log`, `info`, `warn` and
|
|
349
|
+
`error` are captured the same way.
|
|
350
|
+
|
|
351
|
+
**The Rack checks what comes back.** Every record the cutover leaves in place, returned or kept,
|
|
352
|
+
must match the target version's schema and fit its quota. A record cannot change its `scope_id` or `owner_id`, and can move only to
|
|
353
|
+
a collection with the same scope. A new record in a `user` or `room` collection needs the
|
|
354
|
+
`scope_id` of a participant or room that already has records, and its `owner_id` must be `null` or
|
|
355
|
+
someone who already owns one. Records missing from a returned collection are deleted. If any check
|
|
356
|
+
fails, nothing is written and the errors name the collection, the record and the problem. These are
|
|
357
|
+
checks on structure, not on which participant's values end up in which record: migrations see every
|
|
358
|
+
scope's data and are [privileged code](security.md#the-migration-runner).
|
|
359
|
+
|
|
360
|
+
Migrations run in the browser of whoever promotes, in a sandboxed frame with no network access
|
|
361
|
+
(see [Security](security.md#the-migration-runner)), one after another, 30 seconds each by default.
|
|
362
|
+
A migration that throws, times out, lacks the function it needs, or returns anything other than
|
|
363
|
+
`{ collection: records[] }` stops the run, and nothing is written. If the applet's data changes
|
|
364
|
+
while they run, the promotion is refused with `data_changed` and they run again.
|
|
365
|
+
|
|
366
|
+
The Rack keeps a ledger of which migrations have run on each applet, with each one's source. Going
|
|
367
|
+
forward, pending `up`s run in name order. Promoting a version that lacks some applied migrations
|
|
368
|
+
runs their `down`s in reverse, from the stored source, so a rollback works even after a later
|
|
369
|
+
package dropped the file. A migration with no `down` cannot be rolled back; the Rack says so, and
|
|
370
|
+
you restore the snapshot taken before it ran.
|
|
371
|
+
|
|
372
|
+
`attempt` records are never passed in: they stay in the shape their attempt wrote them. Preview
|
|
373
|
+
records are not passed in either.
|
|
374
|
+
|
|
375
|
+
Migrations are plain functions over plain arrays, so test them like any other: import the module,
|
|
376
|
+
call `up` on a fixture, assert on the result. The mock Rack does not run them.
|
|
377
|
+
|
|
378
|
+
<a name="snapshots"></a>
|
|
379
|
+
## Snapshots
|
|
380
|
+
|
|
381
|
+
Before every cutover that changes data, the Rack copies the applet's entire dataset: every record
|
|
382
|
+
in every collection and scope, preview and set-aside ones included, the collection registry, the
|
|
383
|
+
migration ledger and the applet's [role](auth.md#roles) assignments. The copy is written before the
|
|
384
|
+
promotion's transaction opens and marked applied if the cutover commits, so a migration that
|
|
385
|
+
wrecks the data can always be undone.
|
|
386
|
+
|
|
387
|
+
The transaction then locks the applet's collections, which holds record writes off until it
|
|
388
|
+
commits, and checks the data is still what the cutover was planned and snapshotted from. A write
|
|
389
|
+
that landed in between refuses the promotion with `data_changed`, before anything is deleted, so a
|
|
390
|
+
cutover never removes a record its snapshot does not hold. Likewise, a promotion refuses with
|
|
391
|
+
`version_changed` if another one committed while it was being prepared.
|
|
392
|
+
|
|
393
|
+
Snapshots are also taken before every restore, so a restore can itself be undone, and on request:
|
|
394
|
+
|
|
395
|
+
```sh
|
|
396
|
+
php artisan ziprack:db:snapshot my-applet
|
|
397
|
+
php artisan ziprack:db:snapshots my-applet # list them, with ids
|
|
398
|
+
php artisan ziprack:db:restore my-applet 42 --force
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
A restore replaces all of the applet's records, its collection registry, its ledger and its role
|
|
402
|
+
assignments with the snapshot's, then makes the version that was current when it was taken current
|
|
403
|
+
again, if that version is still `ready`. The file's sha256 is checked first; a snapshot that does
|
|
404
|
+
not match is not restored.
|
|
405
|
+
|
|
406
|
+
The file is gzipped NDJSON: a header line (`format: ziprack-snapshot/1`, with the registry, the
|
|
407
|
+
ledger, role assignments and record counts), then one line per record. It is stored at
|
|
408
|
+
`ziprack/snapshots/{slug}/…ndjson.gz` on `ziprack-db.snapshots.disk`, which defaults to
|
|
409
|
+
`ziprack.disk`. **That disk must be persistent** — S3 in production, like packages.
|
|
410
|
+
|
|
411
|
+
The newest `ziprack-db.snapshots.keep` (10) are kept per applet, and never at the expense of the
|
|
412
|
+
newest one taken before a cutover.
|
|
413
|
+
|
|
159
414
|
<a name="how-it-is-stored"></a>
|
|
160
415
|
## How It Is Stored
|
|
161
416
|
|
|
162
417
|
Worth knowing, because it explains the constraints above.
|
|
163
418
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
The
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
419
|
+
Every collection's records live in one shared `ziprack_records` table, created by a migration and
|
|
420
|
+
keyed by collection and scope — the attempt, participant or room id, or `0` for `applet` scope.
|
|
421
|
+
The document lives in a JSON column; filters and ordering on an
|
|
422
|
+
`indexed` path read that path out of the JSON, typed from the declared schema so numbers compare as
|
|
423
|
+
numbers. Every query is narrowed by an index on `(collection_id, scope_id)` first, and the quota caps
|
|
424
|
+
a scope's records, so that read is always over a small, bounded set. The declared schema is still
|
|
425
|
+
the query plan: only `indexed` paths can be filtered or sorted, so there is no need for containment
|
|
426
|
+
queries over undeclared keys.
|
|
427
|
+
|
|
428
|
+
A record written while staff [preview](attempts.md#preview-runs) the applet is stored like any other,
|
|
429
|
+
with `is_preview` set when it is created. The flag never changes after that, so a shared board keeps
|
|
430
|
+
the flag of whoever wrote each card. Applets never see it; it exists for the admin, which leaves
|
|
431
|
+
preview records out unless asked.
|
|
432
|
+
|
|
433
|
+
**Installing or promoting a version never changes the database schema.** Adding a collection or an
|
|
434
|
+
indexed path only writes rows, so both stay one transaction on every database. The collection
|
|
435
|
+
registry follows the current version: promotion updates each definition and sets aside the
|
|
436
|
+
collections the new version no longer declares. They keep their records until you run:
|
|
173
437
|
|
|
174
438
|
```sh
|
|
175
439
|
php artisan ziprack:db:prune
|
|
440
|
+
php artisan ziprack:db:prune --stranded # also the rows described below
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Upgrading from a Rack before v1.8.** Applet-scoped records used to belong to the version that
|
|
444
|
+
wrote them, so every upgrade started from an empty dataset. A migration re-keys the current
|
|
445
|
+
version's rows to the applet; earlier versions' rows are left where they were, unreachable. Bring
|
|
446
|
+
one version's rows back into the live dataset, or delete them all:
|
|
447
|
+
|
|
448
|
+
```sh
|
|
449
|
+
php artisan ziprack:db:recover my-applet 1.2.0 [collection] [--replace] [--force]
|
|
450
|
+
php artisan ziprack:db:prune --stranded
|
|
176
451
|
```
|
|
452
|
+
|
|
453
|
+
`recover` adds to the live records (`--replace` deletes those first), reports how many recovered
|
|
454
|
+
records the current schema rejects, and takes a snapshot before it changes anything.
|
|
455
|
+
|
|
456
|
+
<a name="rack-side"></a>
|
|
457
|
+
## Rack Side
|
|
458
|
+
|
|
459
|
+
| Command | Does |
|
|
460
|
+
|---|---|
|
|
461
|
+
| `ziprack:db:prune [--stranded] [--force]` | Delete set-aside collections and their records; with `--stranded`, pre-v1.8 applet rows too. |
|
|
462
|
+
| `ziprack:db:recover {slug} {version} [collection] [--replace] [--force]` | Bring an earlier version's stranded applet-scoped records back. |
|
|
463
|
+
| `ziprack:db:snapshot {slug}` · `ziprack:db:snapshots {slug}` | Take a snapshot; list them. |
|
|
464
|
+
| `ziprack:db:restore {slug} {snapshot} [--force]` | Replace the applet's data with a snapshot. |
|
|
465
|
+
|
|
466
|
+
The admin API routes — the record browser, the cutover plan and check, snapshots — are listed in
|
|
467
|
+
[Reporting](reporting.md#the-admin-api).
|
|
468
|
+
|
|
469
|
+
A Rack that lets its operators edit records uses `ZipRack\Db\RecordAdmin`: `create()` (into an
|
|
470
|
+
`applet` collection only, since every other scope belongs to a participant, room or attempt),
|
|
471
|
+
`update()`, `setField()` across many records, all or none, and `delete()`. Every write is checked
|
|
472
|
+
against the schema and quota exactly as an applet's write is, and fires the same `RecordChanged`
|
|
473
|
+
event, with a `null` participant, so a live room sees the edit.
|
package/dist/docs/extending.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
- [Message Handlers](#message-handlers)
|
|
8
8
|
- [Launch Payload](#launch-payload)
|
|
9
9
|
- [Install Hooks](#install-hooks)
|
|
10
|
+
- [Promotion Hooks](#promotion-hooks)
|
|
11
|
+
- [Rack Roles](#rack-roles)
|
|
10
12
|
- [Conformance and Status](#conformance-and-status)
|
|
11
13
|
- [The SDK Side](#the-sdk-side)
|
|
12
14
|
|
|
@@ -60,7 +62,8 @@ class SurveyCapability extends AbstractCapability
|
|
|
60
62
|
| `validateManifest()` | Cross-field checks; add errors to the validator. |
|
|
61
63
|
| `messageHandlers()` | `type => handler class`. |
|
|
62
64
|
| `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. |
|
|
65
|
+
| `onInstall()` / `onPromote()` / `onRetire()` | Version lifecycle hooks. `onInstall()` runs inside the install transaction, after extraction, so row changes only. `onPromote()` runs after the promotion commits. |
|
|
66
|
+
| `preparePromotion()` | Only with `ParticipatesInPromotion`: carry state over when the current version changes, or refuse. See [Promotion Hooks](#promotion-hooks). |
|
|
64
67
|
| `adminRoutes()` | Headless admin routes, registered only when `ziprack/admin` is installed. |
|
|
65
68
|
| `conformanceTests()` | Checks contributed to the conformance suite. |
|
|
66
69
|
| `statusChecks()` | Warnings for `ziprack:status` and `/ziprack/health`. |
|
|
@@ -204,13 +207,81 @@ untrusted frame: it is the one place where over-sharing is silent.
|
|
|
204
207
|
```php
|
|
205
208
|
public function onInstall(AppletVersion $version, ?AppletVersion $previous): void
|
|
206
209
|
{
|
|
207
|
-
$this->installer->install($version, $previous); //
|
|
210
|
+
$this->installer->install($version, $previous); // row changes only; no DDL
|
|
208
211
|
}
|
|
209
212
|
```
|
|
210
213
|
|
|
211
|
-
`onInstall()` runs inside the install transaction,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
`onInstall()` runs inside the install transaction, after the package is extracted, with the
|
|
215
|
+
previous current version for comparison. It can read the package's files from
|
|
216
|
+
`$version->storage_path` on `ziprack.disk` — which is how `db@1` checks seed files and migration
|
|
217
|
+
names. Keep it to checking and to rows the live version cannot see: the version is not current yet,
|
|
218
|
+
so anything that changes what running sessions read belongs in [promotion](#promotion-hooks). If it
|
|
219
|
+
throws, the whole installation rolls back and extracted files are removed; throw a
|
|
220
|
+
`PackageInstallException` to give the uploader a message.
|
|
221
|
+
|
|
222
|
+
Keep it to row changes. `CREATE`, `ALTER` and `DROP` commit any open transaction on MySQL and
|
|
223
|
+
MariaDB, which breaks the install and lets the conformance suite's data escape its rollback. If a
|
|
224
|
+
capability needs tables, ship them as migrations; the conformance suite fails any check that commits
|
|
225
|
+
its transaction.
|
|
226
|
+
|
|
227
|
+
<a name="promotion-hooks"></a>
|
|
228
|
+
## Promotion Hooks
|
|
229
|
+
|
|
230
|
+
A capability that holds per-applet state which has to follow the current version — `db@1` and its
|
|
231
|
+
records — implements `ZipRack\Core\Contracts\ParticipatesInPromotion`:
|
|
232
|
+
|
|
233
|
+
```php
|
|
234
|
+
use ZipRack\Core\Contracts\ParticipatesInPromotion;
|
|
235
|
+
|
|
236
|
+
class SurveyCapability extends AbstractCapability implements ParticipatesInPromotion
|
|
237
|
+
{
|
|
238
|
+
public function preparePromotion(AppletVersion $version, ?AppletVersion $outgoing, array $input): ?Closure
|
|
239
|
+
{
|
|
240
|
+
$plan = $this->planner->plan($version, $outgoing);
|
|
241
|
+
|
|
242
|
+
if ($plan->losesAnswers() && ! ($input['accept_loss'] ?? false)) {
|
|
243
|
+
throw new PromotionBlockedException('Version '.$version->version.' drops surveys that have answers.');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return fn () => $this->planner->apply($plan); // inside the promotion transaction
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`PromoteVersion::handle($version, $force, $input)` calls it on every capability the *target*
|
|
252
|
+
version declares, whenever the current version actually changes, forwards or back:
|
|
253
|
+
|
|
254
|
+
1. `preparePromotion()` runs **before** the transaction opens, so slow or file-writing work — `db@1`
|
|
255
|
+
writes its snapshot here — stays out of it. Throw a `PromotionBlockedException` to refuse.
|
|
256
|
+
2. The closures it returned run **inside** the promotion transaction, before `current_version_id`
|
|
257
|
+
moves. Row changes only, for the same DDL reason as `onInstall()`.
|
|
258
|
+
3. After the commit, `onPromote()` runs.
|
|
259
|
+
|
|
260
|
+
`$input` is this capability's share of the promotion input, keyed by its id: the admin API's
|
|
261
|
+
promote body passes `db: {…}` to `db@1`. A refusal that carries something the caller should show
|
|
262
|
+
extends `PromotionBlockedException`; `db@1` throws `ZipRack\Core\Installer\DataCutoverRequired`,
|
|
263
|
+
with a `reason` and the plan as `details()`, which the admin API turns into a `409`.
|
|
264
|
+
|
|
265
|
+
<a name="rack-roles"></a>
|
|
266
|
+
## Rack Roles
|
|
267
|
+
|
|
268
|
+
An applet's declared [roles](auth.md#roles) can be assigned to everyone holding one of the Rack's
|
|
269
|
+
own roles. zipRACK asks the Rack what those are through `ZipRack\Core\Contracts\RackRoles`:
|
|
270
|
+
|
|
271
|
+
```php
|
|
272
|
+
interface RackRoles
|
|
273
|
+
{
|
|
274
|
+
/** @return array<string, string> key => label */
|
|
275
|
+
public function all(): array;
|
|
276
|
+
|
|
277
|
+
/** @return list<string> keys */
|
|
278
|
+
public function of(Authenticatable $user): array;
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
The default, `GateRackRoles`, has one role, `administrator`, from the `ziprack.admin` gate. A Rack
|
|
283
|
+
with authors, facilitators or departments binds its own, the same way it binds
|
|
284
|
+
[`AppletAccess`](reporting.md#who-may-do-what).
|
|
214
285
|
|
|
215
286
|
<a name="conformance-and-status"></a>
|
|
216
287
|
## Conformance and Status
|
|
@@ -85,6 +85,7 @@ ones you will actually touch.
|
|
|
85
85
|
| `asset_token_ttl_hours` | `6` | Lifetime of the signed package-asset token. |
|
|
86
86
|
| `session_ttl_hours` | `6` | Lifetime of a launch session. |
|
|
87
87
|
| `csp.img_hosts`, `csp.media_hosts` | `[]` | Per-deployment allowlist. A bare scheme is rejected. |
|
|
88
|
+
| `player.frame_allow` | `[]` | Features added to the applet iframe's `allow` list, e.g. `microphone`. Bare names from a fixed set only. |
|
|
88
89
|
| `media.providers` | `['vimeo','youtube']` | Drives `frame-src` and manifest validation. |
|
|
89
90
|
| `publishing.require_sbom` | `true` | Refuse packages with no SBOM. |
|
|
90
91
|
| `publishing.require_review_for_new_capabilities` | `true` | Block promotion until a widened `requires` is approved. |
|
|
@@ -98,11 +99,11 @@ ones you will actually touch.
|
|
|
98
99
|
|---|---|
|
|
99
100
|
| `ziprack-auth.php` | `releasable` — the single place a deployment answers "what can applets see about our people"; `claim_map` |
|
|
100
101
|
| `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-db.php` | `max_records_per_scope` (5000), `max_record_bytes` (64 KB), `max_indexed_paths` (8), `list_limit` (100), `snapshots.disk` (`ziprack.disk`; must be persistent), `snapshots.keep` (10) |
|
|
102
103
|
| `ziprack-storage.php` | `disk`, `max_bytes_cap` (50 MB), `url_ttl_minutes` (10), `scanner` (`null` \| `clamav`) |
|
|
103
104
|
| `ziprack-ai.php` | `provider`, `models.analysis`, `models.chat`, `allowed_models`, the whole `budget` block |
|
|
104
105
|
| `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
|
+
| `ziprack-admin.php` | `prefix` (`ziprack/admin`), `middleware` (`web`, `auth`, `can:ziprack.use-admin`) |
|
|
106
107
|
|
|
107
108
|
Ceilings in these files are **hard**: a manifest asking for a larger quota or budget fails
|
|
108
109
|
validation. Defaults apply when a manifest declares nothing.
|
|
@@ -121,6 +122,7 @@ validation. Defaults apply when a manifest declares nothing.
|
|
|
121
122
|
| `GET /ziprack/f/{file}` | Signed, short-lived download of a stored file (`storage`). |
|
|
122
123
|
| `POST /ziprack/s/{session}/broadcast/auth` | Rack-side WebSocket channel authentication (`broadcast`). |
|
|
123
124
|
| `GET /ziprack/health` | Fleet visibility: format, core version, capabilities, warnings. |
|
|
125
|
+
| `GET /ziprack/migration-runner` | The no-network page a Rack UI frames, sandboxed, to run [data migrations](database.md#data-migrations) at promotion. |
|
|
124
126
|
| `GET /r/{code}` | Join a room by code (`broadcast`). |
|
|
125
127
|
| `*` `/ziprack/admin/*` | The headless admin API (`ziprack/admin`). |
|
|
126
128
|
|
|
@@ -171,8 +173,8 @@ the deployment owns. A Rack can add an approved image host; it cannot reintroduc
|
|
|
171
173
|
|
|
172
174
|
| Command | Does |
|
|
173
175
|
|---|---|
|
|
174
|
-
| `ziprack:install {zip} [--
|
|
175
|
-
| `ziprack:promote {slug} {version} [--force]` | Make a version current. `--force` also approves a pending capability review. |
|
|
176
|
+
| `ziprack:install {zip} [--slug=] [--activate] [--force]` | Install a package as a new version; `--slug` installs into that existing applet. `--activate` promotes it, `--force` past a pending capability review. |
|
|
177
|
+
| `ziprack:promote {slug} {version} [--force] [--accept-data-loss] [--keep-invalid]` | Make a version current. `--force` also approves a pending capability review; `--accept-data-loss` confirms a [data cutover](publishing.md#the-data-cutover) that rejects or drops records, and `--keep-invalid` keeps rejected records instead of deleting them. |
|
|
176
178
|
| `ziprack:retire {slug} {version}` | Kill switch: the version can no longer be launched. |
|
|
177
179
|
| `ziprack:status [--json]` | Format, core version, installed capabilities, warnings. |
|
|
178
180
|
| `ziprack:conformance [--json]` | Verify this Rack honours `ziprack/1`. |
|
|
@@ -181,5 +183,9 @@ the deployment owns. A Rack can add an approved image host; it cannot reintroduc
|
|
|
181
183
|
| `ziprack:roster:import {csv}` | Import profile attributes by email. |
|
|
182
184
|
| `ziprack:room:create {slug} [--name=] [--mode=] [--roster=]` | Create a room and print its join code. |
|
|
183
185
|
| `ziprack:rooms:tick` | Vacate and reassign dropped facilitators. **Schedule every minute.** |
|
|
184
|
-
| `ziprack:db:prune [--force]` |
|
|
186
|
+
| `ziprack:db:prune [--stranded] [--force]` | Delete collections no current manifest declares, and their records. `--stranded` also deletes applet-scoped rows earlier versions left behind before v1.8. |
|
|
187
|
+
| `ziprack:db:recover {slug} {version} [collection] [--replace] [--force]` | Bring those stranded rows back into the live dataset. |
|
|
188
|
+
| `ziprack:db:snapshot {slug}` · `ziprack:db:snapshots {slug}` | Snapshot an applet's data; list its snapshots. |
|
|
189
|
+
| `ziprack:db:restore {slug} {snapshot} [--force]` | Replace an applet's data with a snapshot and make that version current again. |
|
|
190
|
+
| `ziprack:mcp:token {email} [--name=]` · `ziprack:mcp:revoke {email} [token] [--all]` | Mint and revoke [MCP](mcp.md) tokens. |
|
|
185
191
|
| `ziprack:response-analysis [--applet=] [--force]` | Regenerate admin AI summaries. **Schedule every few hours.** |
|
|
@@ -40,7 +40,7 @@ The iframe is rendered with:
|
|
|
40
40
|
|
|
41
41
|
```html
|
|
42
42
|
<iframe sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
|
|
43
|
-
allow="autoplay; fullscreen; picture-in-picture; encrypted-media">
|
|
43
|
+
allow="autoplay; fullscreen; picture-in-picture; encrypted-media; clipboard-write">
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Note the absence of `allow-same-origin`. The applet document has an **opaque origin**: no cookies,
|
|
@@ -72,11 +72,12 @@ ever cross the boundary.
|
|
|
72
72
|
| **Applet** | One experience or app, identified by an immutable `slug`. Has many versions. | core |
|
|
73
73
|
| **Version** | One uploaded zip. Immutable. Exactly one is current. | core |
|
|
74
74
|
| **Participant** | The person in front of the applet, resolved from the Rack's own users (or a guest cookie). | core |
|
|
75
|
+
| **Role** | A name the participant holds in an applet: built in (`member`, `guest`, `applet-author`, `rack-administrator`) or declared by the manifest and assigned by the Rack. Sent at launch. | core |
|
|
75
76
|
| **Attempt** | One participant's run. Holds `status`, `state`, `segment`. Pinned to the version it started on. | `attempt@1` |
|
|
76
77
|
| **Statement** | Append-only record: actor did verb on activity with result. The audit trail and the uniform export. | `activity@1` |
|
|
77
|
-
| **Record** | A mutable row in a declared collection, at a declared scope. Working state. | `db@1` |
|
|
78
|
+
| **Record** | A mutable row in a declared collection, at a declared scope. Working state that carries over to new versions. | `db@1` |
|
|
78
79
|
| **Stored file** | A participant upload, addressable only once scanned clean. | `storage@1` |
|
|
79
|
-
| **Room** | A shared session: an applet
|
|
80
|
+
| **Room** | A shared session: an applet plus its members, each `participant` or `facilitator`. | `broadcast@1` |
|
|
80
81
|
| **Profile attribute** | A Rack-owned fact about the participant, released only if requested and permitted. | `auth@1` |
|
|
81
82
|
|
|
82
83
|
Statements and records coexist because they answer different questions. A statement is *what
|
|
@@ -103,8 +104,9 @@ any call the manifest did not declare.
|
|
|
103
104
|
Capability versions are independent of the format string and of the Composer package version. See
|
|
104
105
|
[Versioning](versioning.md).
|
|
105
106
|
|
|
106
|
-
|
|
107
|
-
`Rack.eject()`, which asks the Rack to
|
|
107
|
+
Three things need no capability: [declared media](media.md) (`Rack.media.embedUrl(id)`),
|
|
108
|
+
[roles](auth.md#roles) (`Rack.auth.hasRole(name)`), and `Rack.eject()`, which asks the Rack to
|
|
109
|
+
leave the applet.
|
|
108
110
|
|
|
109
111
|
<a name="what-ziprack-is-not"></a>
|
|
110
112
|
## What zipRACK Is Not
|
package/dist/docs/manifest.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
- [Introduction](#introduction)
|
|
4
4
|
- [Core Fields](#core-fields)
|
|
5
5
|
- [The Requires Block](#the-requires-block)
|
|
6
|
+
- [Roles](#roles)
|
|
6
7
|
- [Capability Sections](#capability-sections)
|
|
7
8
|
- [A Complete Example](#a-complete-example)
|
|
8
9
|
- [Validation Errors](#validation-errors)
|
|
@@ -42,6 +43,7 @@ upload meaningful: the manifest is a contract a reviewer can read.
|
|
|
42
43
|
| `entry` | Optional, default `index.html`. Relative, no `..`, ends in `.html`. |
|
|
43
44
|
| `requires` | Required, even if empty. See below. |
|
|
44
45
|
| `media` | Optional. See [Media](media.md). |
|
|
46
|
+
| `roles` | Optional. The applet's own roles. See [Roles](#roles). |
|
|
45
47
|
|
|
46
48
|
Everything else at the top level belongs to a capability. A section whose owning capability is not
|
|
47
49
|
declared is a validation error, and so is a section no installed capability owns.
|
|
@@ -64,6 +66,28 @@ declared is a validation error, and so is a section no installed capability owns
|
|
|
64
66
|
- Declaring `attempt@1` has a side effect: it pins every participant to the version they started on.
|
|
65
67
|
See [Versioning](versioning.md#version-pinning).
|
|
66
68
|
|
|
69
|
+
<a name="roles"></a>
|
|
70
|
+
## Roles
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
"roles": [
|
|
74
|
+
{ "name": "dungeon-master", "label": "Dungeon Master", "description": "Runs the session." },
|
|
75
|
+
{ "name": "spectator", "grant": "guests" }
|
|
76
|
+
]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A core key, so it needs no capability. Every launch tells the applet which roles the participant
|
|
80
|
+
holds; the Rack decides who holds them. See [Roles](auth.md#roles) for how they resolve.
|
|
81
|
+
|
|
82
|
+
| Field | Rules |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `name` | Required. Lowercase words joined by hyphens, `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`, max 40, unique. Not one of the built-in roles: `rack-administrator`, `applet-author`, `member`, `guest`. |
|
|
85
|
+
| `label` | Optional, max 60. Shown to whoever assigns the role. Defaults to `name`. |
|
|
86
|
+
| `description` | Optional, max 255. |
|
|
87
|
+
| `grant` | Optional. Who holds it before anyone is assigned: `everyone`, `members`, `guests` or `assigned` (the default — nobody). |
|
|
88
|
+
|
|
89
|
+
At most 20 roles.
|
|
90
|
+
|
|
67
91
|
<a name="capability-sections"></a>
|
|
68
92
|
## Capability Sections
|
|
69
93
|
|
|
@@ -72,7 +96,7 @@ declared is a validation error, and so is a section no installed capability owns
|
|
|
72
96
|
| `segments[]` | `attempt@1` | `{key, label, values[{key,label}]}` — dimensions the admin filters by |
|
|
73
97
|
| `activities[]` | `activity@1` | `{id, type, label, scale?, segment?, sequence?}` — required, at least one |
|
|
74
98
|
| `reporting.metrics[]` | `activity@1` (+ `attempt@1`) | `{key, label, pre, post}` — pairs two declared activities |
|
|
75
|
-
| `collections[]` | `db@1` | `{name, scope, schema, indexed?, quota?, permissions?}` |
|
|
99
|
+
| `collections[]` | `db@1` | `{name, scope, schema, indexed?, quota?, permissions?, seed?}` |
|
|
76
100
|
| `uploads[]` | `storage@1` | `{name, scope, mime[], max_bytes?, max_files?}` |
|
|
77
101
|
| `prompts[]` | `ai@1` | `{id, mode, template, vars?, output_schema?, budget?, model?}` |
|
|
78
102
|
| `broadcast` | `broadcast@1` | `{channels?, facilitator_events?}` |
|
|
@@ -111,6 +135,10 @@ An applet exercising every capability at once. Most applets need three or four.
|
|
|
111
135
|
{ "id": "debrief.clip", "provider": "youtube", "key": "dQw4w9WgXcQ" }
|
|
112
136
|
],
|
|
113
137
|
|
|
138
|
+
"roles": [
|
|
139
|
+
{ "name": "judge", "label": "Judge", "description": "Scores the final pitches." }
|
|
140
|
+
],
|
|
141
|
+
|
|
114
142
|
"segments": [
|
|
115
143
|
{
|
|
116
144
|
"key": "track",
|
|
@@ -204,6 +232,11 @@ one message. The checks that catch people out:
|
|
|
204
232
|
| `reporting.metrics needs the attempt capability` | Metrics are computed over completed attempts. |
|
|
205
233
|
| `A room-scoped collection requires the [broadcast] capability` | Scope implies capability. |
|
|
206
234
|
| `Indexed path [votes] must be a string, integer, number or boolean property declared in the schema` | You can only index what you declared, and only scalars. |
|
|
235
|
+
| `Only applet-scoped collections can be seeded` | A seed is shared reference data; see [Seeding](database.md#seeding). |
|
|
236
|
+
| `Role names are lowercase words joined by hyphens, like "dungeon-master".` | `roles[].name` has capitals, underscores or spaces. |
|
|
237
|
+
| `Role [guest] is one every Rack provides …; it cannot be declared.` | `rack-administrator`, `applet-author`, `member` and `guest` are built in. |
|
|
238
|
+
| `Duplicate role [judge].` | Role names are unique. |
|
|
239
|
+
| `A role's grant is one of everyone, members, guests or assigned.` | Any other `grant`. |
|
|
207
240
|
| `Placeholder {{pitch}} is not declared in vars.properties` | Prompt templates and their `vars` schema must agree. |
|
|
208
241
|
| `Analysis prompts need an output_schema of type object` | `mode: "analysis"` is structured output by definition. |
|
|
209
242
|
| `The $room channel is reserved for the Rack` | Pick another channel name. |
|