@ziprack/sdk 0.2.1 → 0.4.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/ai/index.d.ts +36 -4
- package/dist/auth/index.d.ts +11 -0
- package/dist/core/mock/MockRack.d.ts +22 -0
- package/dist/core/rack.d.ts +9 -0
- package/dist/core/types.d.ts +13 -0
- package/dist/docs/README.md +3 -3
- package/dist/docs/ai.md +84 -0
- package/dist/docs/applet-packages.md +18 -8
- 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 +253 -10
- package/dist/docs/extending.md +70 -4
- package/dist/docs/installation.md +14 -6
- package/dist/docs/introduction.md +7 -5
- package/dist/docs/manifest.md +34 -1
- package/dist/docs/mcp.md +18 -3
- package/dist/docs/protocol.md +42 -0
- package/dist/docs/publishing.md +87 -8
- package/dist/docs/reporting.md +11 -4
- package/dist/docs/sdk.md +28 -4
- package/dist/docs/security.md +61 -4
- 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 +43 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +67 -33
- package/dist/index.js.map +1 -1
- package/dist/{store-BXGon4iJ.js → store-BHnO2KB3.js} +29 -17
- package/dist/store-BHnO2KB3.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/protocol.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
- [Streaming](#streaming)
|
|
8
8
|
- [Events](#events)
|
|
9
9
|
- [What the Shell Does](#what-the-shell-does)
|
|
10
|
+
- [The Migration Runner](#the-migration-runner)
|
|
10
11
|
|
|
11
12
|
<a name="when-you-need-this"></a>
|
|
12
13
|
## When You Need This
|
|
@@ -51,6 +52,10 @@ The applet announces readiness as early as it can; the host replies with the ful
|
|
|
51
52
|
(see [Launch Data](sdk.md#launch-data)). Do not send capability calls before `rack:launch` arrives —
|
|
52
53
|
queue them, as the SDK does.
|
|
53
54
|
|
|
55
|
+
The core of the payload is `schema`, `actor`, `applet`, `capabilities`, `media` and `roles` — the
|
|
56
|
+
participant's [roles](auth.md#roles) in this applet, as a list of names, sent whatever the manifest
|
|
57
|
+
declares. Each declared capability adds its own key.
|
|
58
|
+
|
|
54
59
|
<a name="calls-and-acks"></a>
|
|
55
60
|
## Calls and Acks
|
|
56
61
|
|
|
@@ -119,3 +124,40 @@ Knowing its behaviour explains timings you would otherwise find surprising:
|
|
|
119
124
|
`/ziprack/s/{session}/stream/{requestId}` and relays each chunk as a partial ack.
|
|
120
125
|
- **Completion.** A successful `attempt:complete` flips the Rack's own completion chrome — which is
|
|
121
126
|
why a completed attempt lands on its final screen rather than restarting.
|
|
127
|
+
|
|
128
|
+
<a name="the-migration-runner"></a>
|
|
129
|
+
## The Migration Runner
|
|
130
|
+
|
|
131
|
+
A second, unrelated frame protocol, between the Rack's own UI and the page that runs a package's
|
|
132
|
+
[data migrations](database.md#data-migrations) at promotion. No applet is involved: the parent is
|
|
133
|
+
the Rack page promoting the version, the child is `GET /ziprack/migration-runner` (route
|
|
134
|
+
`ziprack.migration-runner`), framed sandboxed (see [Security](security.md#the-migration-runner)).
|
|
135
|
+
You need this only if you are building a Rack UI.
|
|
136
|
+
|
|
137
|
+
Messages are plain objects, not `ziprack` envelopes. Everything the runner posts carries
|
|
138
|
+
`ziprackMigrationRunner: true`; it accepts messages only from `window.parent`, and runs once per
|
|
139
|
+
page load.
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
runner → parent { ziprackMigrationRunner: true, type: "ready" }
|
|
143
|
+
parent → runner { type: "run", steps, collections, context, timeoutMs? }
|
|
144
|
+
runner → parent { ziprackMigrationRunner: true, type: "log", step, level, text } … as it runs
|
|
145
|
+
runner → parent { ziprackMigrationRunner: true, type: "done", collections, ms }
|
|
146
|
+
or { ziprackMigrationRunner: true, type: "error", step, code, message, stack? }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Field | |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `steps` | `[{ name, direction: "up" \| "down", source }]`, in run order — the plan's steps from `GET …/cutover`. |
|
|
152
|
+
| `collections` | `{ name: { scope, records } }` — `collections` from `GET …/cutover/data`. |
|
|
153
|
+
| `context` | `{ applet, from, to }`: the slug and the outgoing and target version strings. |
|
|
154
|
+
| `timeoutMs` | Per step. Default 30000. |
|
|
155
|
+
| `level` | `meta`, `step`, `ok`, `fail`, `log`, `info`, `warn` or `error`. |
|
|
156
|
+
| `code` | `missing_up`, `missing_down` (the migration has no such function), `threw`, `timeout`, `bad_result` (it returned something other than `{ collection: records[] }`). |
|
|
157
|
+
|
|
158
|
+
Each step's source is imported as a module from a `blob:` URL and called with a copy of the
|
|
159
|
+
records; a returned object replaces the collections it names. `done.collections` holds every
|
|
160
|
+
collection, by name, as a list of records: pass it to `POST …/cutover/check` and then, with the
|
|
161
|
+
plan's fingerprint and the logged lines, to `…/promote` as `db.records`, `db.fingerprint` and
|
|
162
|
+
`db.log`. After an `error`, nothing is promoted. The runner also renders every line as a terminal,
|
|
163
|
+
so the parent can show the frame itself as the log.
|
package/dist/docs/publishing.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
- [Installing a Version](#installing-a-version)
|
|
5
5
|
- [Capability Diff Review](#capability-diff-review)
|
|
6
6
|
- [Promotion](#promotion)
|
|
7
|
+
- [The Data Cutover](#the-data-cutover)
|
|
7
8
|
- [The Review Queue](#the-review-queue)
|
|
8
9
|
- [Retiring a Version](#retiring-a-version)
|
|
9
10
|
- [The Audit Log](#the-audit-log)
|
|
@@ -27,12 +28,15 @@ php artisan ziprack:install dist-zip/my-applet-1.0.0.zip --activate # install
|
|
|
27
28
|
|
|
28
29
|
or `POST /ziprack/admin/versions/upload` with the zip.
|
|
29
30
|
|
|
30
|
-
Installation validates the manifest against this Rack's capability set,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
to
|
|
31
|
+
Installation validates the manifest against this Rack's capability set, then, inside one
|
|
32
|
+
transaction, records the version, extracts its files and runs each declared capability's
|
|
33
|
+
`onInstall()` hook — after extraction, so a hook can read the package. `db@1` uses it to check seed
|
|
34
|
+
files and migration names and to register collection names the applet has never had; it does not
|
|
35
|
+
touch existing data. A failure anywhere rolls the transaction back and deletes the extracted
|
|
36
|
+
directory. A version arrives as `ready` but **not current**, unless it is the applet's first —
|
|
37
|
+
there is nothing to review a first version against, so it is promoted automatically.
|
|
34
38
|
|
|
35
|
-
Once
|
|
39
|
+
Once installed the Rack resolves declared media (the oEmbed lookup) and, when
|
|
36
40
|
`publishing.osv_lookup` is on, queues a batch of the SBOM's purls against OSV.dev. Findings attach
|
|
37
41
|
to the version and surface in the review queue; they do not block the install.
|
|
38
42
|
|
|
@@ -74,9 +78,84 @@ php artisan ziprack:promote my-applet 1.1.0 --force # approve the pending rev
|
|
|
74
78
|
or `POST /ziprack/admin/applets/{applet}/versions/{version}/approve` followed by
|
|
75
79
|
`…/promote`.
|
|
76
80
|
|
|
77
|
-
Promotion is refused when the version is not `ready`,
|
|
78
|
-
addition — the error names exactly what was added
|
|
79
|
-
|
|
81
|
+
Promotion is refused when the version is not `ready`, when it has an unapproved capability
|
|
82
|
+
addition — the error names exactly what was added — or when carrying the applet's data over needs
|
|
83
|
+
a decision (below). Otherwise the capabilities' promotion work and the move of `current_version_id`
|
|
84
|
+
commit in one transaction, then each capability's `onPromote()` hook runs.
|
|
85
|
+
|
|
86
|
+
<a name="the-data-cutover"></a>
|
|
87
|
+
## The Data Cutover
|
|
88
|
+
|
|
89
|
+
A version declaring `db@1` brings the applet's records over with it (see
|
|
90
|
+
[Data Across Versions](database.md#data-across-versions)). The Rack plans the cutover first, and
|
|
91
|
+
refuses with a reason while the plan needs a decision:
|
|
92
|
+
|
|
93
|
+
| Reason | Means | To promote |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `migration_required` | [Data migrations](database.md#data-migrations) have to run: the version's pending `up`s, or `down`s for applied migrations it lacks. | Promote from the Rack's UI, which runs them in the browser. |
|
|
96
|
+
| `data_changed` | The data changed between planning the cutover (or running the migrations) and committing it. | Review the plan and promote again; run the migrations again. |
|
|
97
|
+
| `version_changed` | Another promotion committed while this one was being prepared. | Review the new plan and promote again. |
|
|
98
|
+
| `migration_invalid` | What the migrations returned does not fit the version. The plan carries the check. | Fix the migration and upload a new version. |
|
|
99
|
+
| `data_loss` | No migration runs, and the new schema or quota rejects existing records, or a dropped collection holds records. | Confirm with `accept_data_loss`. |
|
|
100
|
+
|
|
101
|
+
Over the admin API the refusal is a `409`:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"message": "Version 2.0.0 changes collections that hold data. Review what changes and confirm.",
|
|
106
|
+
"reason": "data_loss",
|
|
107
|
+
"plan": {
|
|
108
|
+
"version": { "id": 12, "version": "2.0.0" },
|
|
109
|
+
"from": { "id": 9, "version": "1.4.0" },
|
|
110
|
+
"status": "confirm",
|
|
111
|
+
"needs_browser": false,
|
|
112
|
+
"rollback": false,
|
|
113
|
+
"steps": [],
|
|
114
|
+
"collections": [
|
|
115
|
+
{
|
|
116
|
+
"name": "questions", "scope": "applet", "change": "changed", "records": 240,
|
|
117
|
+
"diff": [{ "path": "difficulty", "change": "required_property_added", "detail": "New required property difficulty." }],
|
|
118
|
+
"invalid": 240, "invalid_samples": [{ "id": "…", "errors": { "difficulty": ["…"] } }],
|
|
119
|
+
"over_quota": 0, "seed": "skipped"
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"notes": [{ "level": "warning", "message": "240 of 240 records in [questions] do not match the new schema." }],
|
|
123
|
+
"fingerprint": "…"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
and the decision goes back in the promote body under `db`:
|
|
129
|
+
|
|
130
|
+
| Key | |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `accept_data_loss` | Confirm a `data_loss` plan. A JSON boolean; anything else is a `422`. |
|
|
133
|
+
| `invalid` | `discard` (the default) deletes the records the new schema rejects; `keep` leaves them. |
|
|
134
|
+
| `records`, `fingerprint`, `log` | A migrating version: what the migrations returned, by collection; the fingerprint of the data they ran on; the runner's log, kept with the snapshot. |
|
|
135
|
+
|
|
136
|
+
`GET …/versions/{version}/cutover` returns the same plan without promoting, including each step's
|
|
137
|
+
source. A Rack UI promoting a migrating version reads `…/cutover/data` (the records and their
|
|
138
|
+
fingerprint), runs the steps in the [migration runner](protocol.md#the-migration-runner), checks the
|
|
139
|
+
result with `POST …/cutover/check` (`{records}` → `{ok, errors, collections}`), then promotes with
|
|
140
|
+
`db.records` and `db.fingerprint`. The server checks the records again inside the transaction.
|
|
141
|
+
|
|
142
|
+
From the command line and over MCP a lossy plan can be confirmed but a migration cannot run:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
php artisan ziprack:promote my-applet 2.0.0 --accept-data-loss
|
|
146
|
+
php artisan ziprack:promote my-applet 2.0.0 --accept-data-loss --keep-invalid
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Every cutover that changes data is preceded by a [snapshot](database.md#snapshots).
|
|
150
|
+
|
|
151
|
+
**Uploading with activate.** `ziprack:install --activate`, an admin upload with `activate`, and
|
|
152
|
+
MCP's `create-upload` with `activate: true` try the promotion straight after installing, with no
|
|
153
|
+
cutover decision. If the cutover needs one, the version is installed but not current: the admin
|
|
154
|
+
upload and the MCP upload answer `201` with a `warning` and `cutover: {reason, plan}`, and the
|
|
155
|
+
command prints the refusal and exits non-zero. Promote it once the decision is made.
|
|
156
|
+
|
|
157
|
+
A version that drops `db@1` still goes through the cutover: the applet's collections all count as
|
|
158
|
+
removed, so promoting it needs the same confirmation as removing them one by one.
|
|
80
159
|
|
|
81
160
|
What promotion means depends on pinning:
|
|
82
161
|
|
package/dist/docs/reporting.md
CHANGED
|
@@ -32,7 +32,8 @@ GET /ziprack/admin/applets/{applet}/panels
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
`panels` is the intersection of what the current version declares and what this Rack implements —
|
|
35
|
-
render exactly those and nothing else.
|
|
35
|
+
render exactly those and nothing else. It also contains `roles` when the current version declares
|
|
36
|
+
any [roles](auth.md#roles), for the screen that assigns them.
|
|
36
37
|
|
|
37
38
|
| Declared | Renders |
|
|
38
39
|
|---|---|
|
|
@@ -56,9 +57,10 @@ All routes sit under `ziprack-admin.prefix` (default `/ziprack/admin`) behind `w
|
|
|
56
57
|
| `GET applets` · `POST applets` | List and create |
|
|
57
58
|
| `GET applets/{applet}` · `PATCH applets/{applet}` | Show and update |
|
|
58
59
|
| `GET applets/{applet}/panels` | What to render |
|
|
59
|
-
| `
|
|
60
|
+
| `GET applets/{applet}/roles` · `PUT applets/{applet}/roles/{role}` | Declared roles and their assignments; replace one role's ([Roles](auth.md#rack-side)) |
|
|
61
|
+
| `POST versions/upload` · `POST applets/{applet}/versions` | Install a package; with `activate`, a blocked promotion answers `201` with `warning` (and `cutover`) |
|
|
60
62
|
| `GET applets/{applet}/versions/{version}` | Manifest, requires diff, SBOM, OSV findings, publisher |
|
|
61
|
-
| `POST …/{version}/approve` · `…/promote` · `…/retire` | The publishing actions |
|
|
63
|
+
| `POST …/{version}/approve` · `…/promote` · `…/retire` | The publishing actions. `promote` takes `force` and `db`, and answers `409` when the [data cutover](publishing.md#the-data-cutover) needs a decision |
|
|
62
64
|
| `GET review-queue` | Everything awaiting a decision |
|
|
63
65
|
| `GET participants` · `GET audit-log` | People and capability calls |
|
|
64
66
|
|
|
@@ -69,6 +71,8 @@ Capability packages add their own, and only when installed:
|
|
|
69
71
|
| `GET applets/{applet}/overview` · `…/attempts` · `…/attempts/{attempt}` | `attempt` |
|
|
70
72
|
| `GET applets/{applet}/attempts/{attempt}/statements` · `…/export.csv` · `…/metrics` | `activity` |
|
|
71
73
|
| `GET applets/{applet}/collections` · `…/collections/{name}/records` | `db` |
|
|
74
|
+
| `GET applets/{applet}/versions/{version}/cutover` · `…/cutover/data` · `POST …/cutover/check` | `db` — the cutover plan, the records migrations run on, a check of what they return |
|
|
75
|
+
| `GET applets/{applet}/snapshots` · `POST …/snapshots` · `GET …/snapshots/{snapshot}/log` · `…/download` · `POST …/restore` | `db` — [snapshots](database.md#snapshots); a restore requires `confirm` set to the applet's slug |
|
|
72
76
|
| `GET applets/{applet}/files` | `storage` |
|
|
73
77
|
| `GET applets/{applet}/ai/spend` · `…/ai/calls` | `ai` |
|
|
74
78
|
| `GET applets/{applet}/rooms` · `POST rooms` · `GET rooms/{room}` · `POST rooms/{room}/status` · `GET rooms/{room}/events` | `broadcast` |
|
|
@@ -84,7 +88,7 @@ zipRACK asks, and the Rack answers. Every check goes through one contract,
|
|
|
84
88
|
|---|---|---|
|
|
85
89
|
| `UseAdmin` | `ziprack.use-admin` | The admin API, the MCP server, personal MCP tokens |
|
|
86
90
|
| `CreateApplet` | `ziprack.create-applet` | `POST applets`, `POST versions/upload`, an agent's upload without a slug |
|
|
87
|
-
| `ManageApplet` | `ziprack.manage-applet` | Every `applets/{applet}` route; unpublished and `?preview` player launches |
|
|
91
|
+
| `ManageApplet` | `ziprack.manage-applet` | Every `applets/{applet}` route — promoting, assigning roles, the cutover routes, taking and restoring snapshots; unpublished and `?preview` player launches |
|
|
88
92
|
| `ReviewVersions` | `ziprack.review` | `GET review-queue`, approving, promoting with `force` past review |
|
|
89
93
|
| `ViewRack` | `ziprack.view-rack` | `GET status`, `GET audit-log`, every participant, the `rack-status` tool |
|
|
90
94
|
|
|
@@ -188,3 +192,6 @@ The skeleton ships a reference admin over these endpoints. When you replace it:
|
|
|
188
192
|
2. Render `db@1` record tables from the collection's declared JSON Schema — the schema is there
|
|
189
193
|
precisely so the UI does not need per-applet knowledge.
|
|
190
194
|
3. Treat the review queue as the landing page. It is where the decisions are.
|
|
195
|
+
4. Show the cutover plan before promoting, and run migrations in the sandboxed
|
|
196
|
+
[migration runner](protocol.md#the-migration-runner). A Rack UI is the only place a version with
|
|
197
|
+
data migrations can be promoted.
|
package/dist/docs/sdk.md
CHANGED
|
@@ -64,8 +64,9 @@ interface LaunchData {
|
|
|
64
64
|
applet: { slug: string; name: string; version: string };
|
|
65
65
|
capabilities: string[]; // as declared, e.g. ['auth@1', 'db@1']
|
|
66
66
|
media: Record<string, MediaEmbed>; // Rack-built player URLs by declared id
|
|
67
|
+
roles?: string[]; // e.g. ['member', 'judge']; absent before Rack v1.8
|
|
67
68
|
|
|
68
|
-
auth?: { user: LaunchUser };
|
|
69
|
+
auth?: { user: LaunchUser; withheld?: string[] };
|
|
69
70
|
attempt?: { id, status, state, segment };
|
|
70
71
|
activity?: { activities: LaunchActivity[] };
|
|
71
72
|
db?: { collections: Array<{ name, scope }> };
|
|
@@ -75,9 +76,11 @@ interface LaunchData {
|
|
|
75
76
|
}
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
Each capability contributes its own key, and only when declared.
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
Each capability contributes its own key, and only when declared. `roles` is core and always
|
|
80
|
+
present; read it through `Rack.auth.roles()`, `Rack.auth.hasRole(name)` and `Rack.auth.isGuest()`,
|
|
81
|
+
which need no capability (see [Roles](auth.md#roles)). The typed accessors read from the same
|
|
82
|
+
payload, so `Rack.auth.user()`, `Rack.auth.roles()`, `Rack.attempt.state()` and
|
|
83
|
+
`Rack.activity.activities()` are synchronous and free.
|
|
81
84
|
|
|
82
85
|
```ts
|
|
83
86
|
Rack.on('launch', (data) => render(data)); // fires immediately if already launched
|
|
@@ -178,6 +181,14 @@ Rack.mock.ai('score_pitch', (vars) => ({ clarity: 4, feedback: `Nice work on "${
|
|
|
178
181
|
Rack.mock.resetAttempt(); // replay a completed attempt
|
|
179
182
|
```
|
|
180
183
|
|
|
184
|
+
The mock participant is a `member` holding the manifest's `everyone` and `members` grants. Try
|
|
185
|
+
other identities from the page URL:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
http://localhost:5173/?ziprack-guest a guest, with the guests grants
|
|
189
|
+
http://localhost:5173/?ziprack-roles=judge,applet-author as if the Rack had assigned these
|
|
190
|
+
```
|
|
191
|
+
|
|
181
192
|
Mock mode also engages if a real Rack fails to answer `rack:launch` within `launchTimeoutMs`. That
|
|
182
193
|
is a deliberate development convenience, and the console says so — if you see it in production,
|
|
183
194
|
something is wrong with the shell, not with your applet.
|
|
@@ -214,6 +225,19 @@ it('records the answer and completes exactly once', async () => {
|
|
|
214
225
|
| `host.attempt()`, `host.records(name)`, `host.files()` | Persisted state. |
|
|
215
226
|
| `host.push(event)` | Push a `rack:event` in, as the Rack would, to drive broadcast handlers. |
|
|
216
227
|
|
|
228
|
+
`new FakeHost(manifest, options)` takes the mock Rack's options. Those worth knowing:
|
|
229
|
+
|
|
230
|
+
| Option | |
|
|
231
|
+
|---|---|
|
|
232
|
+
| `guest` | Launch as a guest: `guest` instead of `member`, and the `guests` grants. |
|
|
233
|
+
| `roles` | Roles held on top of the grants, as if assigned. Any name, built-in ones included. |
|
|
234
|
+
| `role` | The room role, `participant` or `facilitator`, instead of the one the assignment mode gives. |
|
|
235
|
+
| `participantId`, `participantName` | The actor. |
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const host = new FakeHost(manifest, { roles: ['judge'] });
|
|
239
|
+
```
|
|
240
|
+
|
|
217
241
|
<a name="leaving-the-applet"></a>
|
|
218
242
|
## Leaving the Applet
|
|
219
243
|
|
package/dist/docs/security.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
- [The Sandbox](#the-sandbox)
|
|
5
5
|
- [The Content Security Policy](#the-content-security-policy)
|
|
6
6
|
- [Tokens and Sessions](#tokens-and-sessions)
|
|
7
|
+
- [The Migration Runner](#the-migration-runner)
|
|
7
8
|
- [Threats the Boundary Does Not Cover](#threats-the-boundary-does-not-cover)
|
|
8
9
|
- [Static Analysis](#static-analysis)
|
|
9
10
|
|
|
@@ -118,12 +119,63 @@ instead of loosening the policy.
|
|
|
118
119
|
scoped to one version, expiring after `ziprack.asset_token_ttl_hours` (6). The extraction disk is
|
|
119
120
|
never publicly served.
|
|
120
121
|
- **Launch session.** Capability calls go to `/ziprack/s/{session}/messages` with a token in the
|
|
121
|
-
path — no session
|
|
122
|
-
`ziprack.session_ttl_hours` (6). The
|
|
122
|
+
path — no Laravel session, no CSRF, rate limited, expiring after
|
|
123
|
+
`ziprack.session_ttl_hours` (6). The token is bound to the launching browser: the launch sets a
|
|
124
|
+
random host-only, HttpOnly, SameSite=Lax `ziprack_launch` cookie (never shared with sibling subdomains), the token carries its hash, and every
|
|
125
|
+
session request must present the matching cookie. A token seen in page source, a screenshot or
|
|
126
|
+
a proxy log cannot be replayed from anywhere else. Logging in or out starts a new cookie, so the next person on a shared
|
|
127
|
+
browser cannot use the previous user's open sessions. The session is what binds a message to a participant, a version,
|
|
123
128
|
an attempt and a room; nothing in the payload can widen that scope.
|
|
124
129
|
- **Signed file URLs.** `storage@1` hands out short-lived URLs, never object keys.
|
|
125
|
-
- **Channel authentication.** The Rack signs its own WebSocket subscriptions
|
|
126
|
-
frame.
|
|
130
|
+
- **Channel authentication.** The Rack signs its own WebSocket subscriptions, and stops signing
|
|
131
|
+
them for a room once it is closed. No token reaches the frame.
|
|
132
|
+
|
|
133
|
+
<a name="the-migration-runner"></a>
|
|
134
|
+
## The Migration Runner
|
|
135
|
+
|
|
136
|
+
[Data migrations](database.md#data-migrations) are package code too, and they run outside the
|
|
137
|
+
applet frame: in the browser of the admin promoting the version, over the applet's whole dataset.
|
|
138
|
+
Two things contain them.
|
|
139
|
+
|
|
140
|
+
**The runner page has no network.** `GET /ziprack/migration-runner` is served with
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline';
|
|
144
|
+
connect-src 'none'; img-src 'none'; font-src 'none'; frame-src 'none'; worker-src blob:;
|
|
145
|
+
form-action 'none'; base-uri 'none'; frame-ancestors 'self'; sandbox allow-scripts
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
and migration code never runs in its window. The page starts a dedicated worker from a `blob:` URL,
|
|
149
|
+
which inherits that CSP, and the worker imports each migration from a `blob:` URL and runs it. A
|
|
150
|
+
worker has no document, no forms and no writable `location`, so migration code cannot navigate a
|
|
151
|
+
frame or a window to carry the data out in a URL, which a CSP alone cannot stop. Under the CSP it
|
|
152
|
+
cannot fetch or open a socket either, and the worker also removes `fetch`, `XMLHttpRequest`,
|
|
153
|
+
`WebSocket`, `EventSource`, `WebTransport` and `importScripts` before it imports anything. Its only
|
|
154
|
+
way out is `postMessage` to the runner page, which relays to the page that framed it. A worker that
|
|
155
|
+
stops responding is terminated. The page holds no data until that parent posts some in, and is never
|
|
156
|
+
publishable.
|
|
157
|
+
|
|
158
|
+
**It always runs sandboxed.** The runner is served from the Rack's own origin, so its CSP carries
|
|
159
|
+
`sandbox allow-scripts`: the browser gives the page an opaque origin however it is framed, and
|
|
160
|
+
migration code cannot reach `window.parent`, the admin's session or the admin API. A Rack UI
|
|
161
|
+
should still frame it with `sandbox="allow-scripts"` (and never `allow-same-origin`), exactly as
|
|
162
|
+
it frames an applet, so the isolation does not rest on one header.
|
|
163
|
+
|
|
164
|
+
**Migrations are privileged.** A migration is handed every carried record of the applet, in every
|
|
165
|
+
`user`, `room` and `applet` scope, and can rewrite any of them. It is trusted the way the package's
|
|
166
|
+
server-side declarations are, not the way applet code is: promoting a version that runs migrations is
|
|
167
|
+
an admin's decision to run that code over everyone's data. The plan carries each migration's source
|
|
168
|
+
(`GET …/versions/{version}/cutover`), so it can be read before it runs.
|
|
169
|
+
|
|
170
|
+
**The server checks the shape of what comes back.** What the migrations return is checked before it
|
|
171
|
+
replaces any data, and again inside the promotion's transaction: the whole resulting dataset,
|
|
172
|
+
collections left out included, must match the target schema and quota; `scope_id` and `owner_id`
|
|
173
|
+
cannot change; a record moves only between collections of the same scope; a new `user` or `room`
|
|
174
|
+
record must name a scope that already has records, and no owner or one who already owns a record.
|
|
175
|
+
These keep every record where it was, but they cannot tell whose values a record holds: a migration
|
|
176
|
+
could copy or swap values between two participants' records and pass. What a migration cannot do is
|
|
177
|
+
send the data anywhere, reach another applet's data, or see `attempt` and preview records. And the
|
|
178
|
+
data it replaced is in the [snapshot](database.md#snapshots) taken before the cutover.
|
|
127
179
|
|
|
128
180
|
<a name="threats-the-boundary-does-not-cover"></a>
|
|
129
181
|
## Threats the Boundary Does Not Cover
|
|
@@ -140,6 +192,11 @@ per month — never advisory numbers.
|
|
|
140
192
|
**Record and storage flooding.** Per-scope quotas, declared in the manifest and capped by the
|
|
141
193
|
deployment regardless of what the manifest asks for.
|
|
142
194
|
|
|
195
|
+
**Trusting roles.** An applet's [roles](auth.md#roles) arrive in the launch payload and are read by
|
|
196
|
+
applet code, which runs in the participant's browser. No capability enforces them server-side yet,
|
|
197
|
+
so a role is a reason to show a control, not a permission. Keep anything a role must protect behind
|
|
198
|
+
a scope, a `permissions` rule or a facilitator event, which the Rack does enforce.
|
|
199
|
+
|
|
143
200
|
**Supply chain.** The SBOM is self-attested. OSV findings catch negligence; the
|
|
144
201
|
[capability diff](publishing.md#capability-diff-review) is what catches the realistic attack, which
|
|
145
202
|
is a benign v1 followed by a quietly wider v2.
|
package/dist/docs/storage.md
CHANGED
|
@@ -29,7 +29,7 @@ An **upload** is a named slot with a scope and a MIME allowlist. Files always be
|
|
|
29
29
|
| Field | Rules |
|
|
30
30
|
|---|---|
|
|
31
31
|
| `name` | Required. `^[a-z][a-z0-9_]*$`, max 40, unique. |
|
|
32
|
-
| `scope` | Required. `attempt`, `user`, `room` or `applet`. Same meaning as [db scopes](database.md#scopes). |
|
|
32
|
+
| `scope` | Required. `attempt`, `user`, `room` or `applet`. Same meaning as [db scopes](database.md#scopes), with one difference: an `applet` slot still belongs to the version, so its files do not carry over to the next one. |
|
|
33
33
|
| `mime` | Required, at least one. `type/subtype` or `type/*`. |
|
|
34
34
|
| `max_bytes` | Optional. Default 10 MB, ceiling 50 MB (`ziprack-storage.max_bytes_cap`). |
|
|
35
35
|
| `max_files` | Optional. Default 20, ceiling 500. |
|
package/dist/docs/studio.md
CHANGED
|
@@ -349,7 +349,7 @@ produces is a document a non-technical colleague can then open and edit.
|
|
|
349
349
|
| `ziprack_compose` | Build a tree from a structure-only skeleton plus content maps. |
|
|
350
350
|
| `ziprack_patch` | `create`, `delete`, `move`, `duplicate`, `props`, `style`, `label`, `settings`, `applet`, `theme`. |
|
|
351
351
|
| `ziprack_validate` | Per-key errors, each with the schema that would have been valid. |
|
|
352
|
-
| `ziprack_derive_manifest` | The manifest this document produces, plus a capability diff against the last build
|
|
352
|
+
| `ziprack_derive_manifest` | The manifest this document produces, plus a capability diff and promotion warnings against the last build: `needs_approval`, `pinning_changed`, `activity_id_changed`, `activity_removed`, `collection_removed`, `collection_changed`. |
|
|
353
353
|
|
|
354
354
|
The SDK's docs tools (`ziprack_guidelines`, `ziprack_list_docs`, `ziprack_search_docs`,
|
|
355
355
|
`ziprack_read_doc`) are served alongside these. See [the SDK's MCP server](sdk.md#the-mcp-server).
|
package/dist/docs/versioning.md
CHANGED
|
@@ -99,3 +99,19 @@ Between versions:
|
|
|
99
99
|
- Adding a capability or a profile attribute blocks promotion until approved.
|
|
100
100
|
- Adding or removing `attempt@1` flips the applet between pinned and rolling, and is flagged
|
|
101
101
|
prominently in the diff.
|
|
102
|
+
- Adding, dropping or reshaping a collection is checked against the data at promotion.
|
|
103
|
+
|
|
104
|
+
### Applet versions and data
|
|
105
|
+
|
|
106
|
+
Versions are immutable; data is not. `db@1` records in `user`, `room` and `applet` scope belong to
|
|
107
|
+
the applet and carry over every promotion, forward or back, so a new version starts from the data
|
|
108
|
+
the last one left. `attempt` records stay with their attempt and the version it ran on.
|
|
109
|
+
|
|
110
|
+
What changes between versions is the *shape* the data must have. At promotion the Rack compares the
|
|
111
|
+
outgoing collections with the new ones, validates the existing records against the new schemas, and
|
|
112
|
+
refuses to lose data silently: a version whose schema rejects existing records, or that drops a
|
|
113
|
+
collection holding records, waits for an explicit confirmation. A version that needs records
|
|
114
|
+
reshaped ships a [data migration](database.md#data-migrations) with `up` and `down`, which the Rack
|
|
115
|
+
runs at promotion and records per applet, and every cutover that changes data is preceded by a
|
|
116
|
+
[snapshot](database.md#snapshots). [Data Across Versions](database.md#data-across-versions) has the
|
|
117
|
+
full list of cases.
|
|
@@ -17,8 +17,9 @@ Follow this document exactly. The Rack rejects packages and messages that break
|
|
|
17
17
|
| Attempt | One participant's run (`attempt@1`). Pinned to the version it started on. Holds `status`, `state`, `segment`. |
|
|
18
18
|
| Room | A shared session: an applet plus its participants (`broadcast@1`). Two roles: `participant`, `facilitator`. |
|
|
19
19
|
| Statement | Append-only "actor did verb on activity with result" (`activity@1`). The audit trail and the uniform export. |
|
|
20
|
-
| Record | A mutable row in a declared collection at a declared scope (`db@1`). Working state. |
|
|
20
|
+
| Record | A mutable row in a declared collection at a declared scope (`db@1`). Working state. Carries over to every new version, except `attempt` scope. |
|
|
21
21
|
| Profile attribute | A Rack-owned fact about the participant, released only if declared and permitted (`auth@1`). |
|
|
22
|
+
| Role | A name the participant holds in this applet: `member` or `guest`, `applet-author`, `rack-administrator`, plus the manifest's own `roles` the Rack grants or assigns. Sent at launch; no capability. |
|
|
22
23
|
|
|
23
24
|
Rules that do not bend:
|
|
24
25
|
|
|
@@ -27,6 +28,7 @@ Rules that do not bend:
|
|
|
27
28
|
3. **Answers are statements, not state.** State is for resuming position. Every committed answer is `Rack.activity.answer()`.
|
|
28
29
|
4. **Resume must work.** Read `Rack.attempt.state()` and `Rack.attempt.status` at launch and put the participant back where they were.
|
|
29
30
|
5. **Degrade gracefully.** A requested profile attribute may be withheld. A room may have no facilitator yet. Design for absence.
|
|
31
|
+
6. **Your data outlives your version.** `user`, `room` and `applet` records carry over every promotion. Change a collection's shape compatibly, or ship a data migration.
|
|
30
32
|
|
|
31
33
|
## 2. Package layout
|
|
32
34
|
|
|
@@ -35,6 +37,7 @@ Rules that do not bend:
|
|
|
35
37
|
├── manifest.json required, at the zip root
|
|
36
38
|
├── sbom.json required: CycloneDX (or package-lock.json)
|
|
37
39
|
├── index.html the entry (configurable via manifest.entry)
|
|
40
|
+
├── migrations/… optional: data migrations, YYYY_MM_DD_HHMMSS_name.js, kept forever
|
|
38
41
|
└── assets/… everything the entry references, by RELATIVE path
|
|
39
42
|
```
|
|
40
43
|
|
|
@@ -71,6 +74,7 @@ Installer rules: static files only (`html js mjs css map json wasm txt vtt svg p
|
|
|
71
74
|
| `requires.capabilities` | Required (may be empty). Each `id@version`; the Rack must implement every one. |
|
|
72
75
|
| `requires.profile` | Attribute keys, only with `auth@1`. Withheld keys are simply absent. |
|
|
73
76
|
| `media[]` | `provider` in the Rack's allowlist (vimeo, youtube); `key` `^\d+$` (Vimeo) or `^[\w-]{11}$` (YouTube). The Rack builds the URL. |
|
|
77
|
+
| `roles[]` | Optional, no capability. `{name, label?, description?, grant?}`, max 20. `name` `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`, max 40, not `rack-administrator`, `applet-author`, `member` or `guest`. `grant`: `everyone`, `members`, `guests` or `assigned` (default: only whom the Rack assigns). |
|
|
74
78
|
|
|
75
79
|
Every other top-level block belongs to a capability and is invalid unless that capability is declared:
|
|
76
80
|
|
|
@@ -78,12 +82,37 @@ Every other top-level block belongs to a capability and is invalid unless that c
|
|
|
78
82
|
|---|---|---|
|
|
79
83
|
| `segments[]` | `attempt@1` | `{key, label, values[{key,label}]}`; the first segment drives admin filtering |
|
|
80
84
|
| `activities[]`, `reporting.metrics[]` | `activity@1` | as in cn-sim/1: `{id, type, label, scale?, segment?, sequence?}`; metrics pair a pre and post rating and need `attempt@1` |
|
|
81
|
-
| `collections[]` | `db@1` | `{name, scope, schema, indexed?, quota?, permissions?}` |
|
|
85
|
+
| `collections[]` | `db@1` | `{name, scope, schema, indexed?, quota?, permissions?, seed?}`; `seed` (applet scope only) loads at promotion into an empty collection |
|
|
82
86
|
| `uploads[]` | `storage@1` | `{name, scope, mime[], max_bytes?, max_files?}` |
|
|
83
87
|
| `prompts[]` | `ai@1` | `{id, mode, template, vars?, output_schema?, budget?}` |
|
|
84
88
|
| `broadcast` | `broadcast@1` | `{channels?, facilitator_events?}` |
|
|
85
89
|
|
|
86
|
-
Scopes (`db@1`, `storage@1`): `attempt` (frozen at completion; needs `attempt@1`), `user` (outlives attempts), `room` (shared; needs `broadcast@1`), `applet` (
|
|
90
|
+
Scopes (`db@1`, `storage@1`): `attempt` (frozen at completion; needs `attempt@1`), `user` (outlives attempts), `room` (shared; needs `broadcast@1`), `applet` (shared by everyone; read-mostly). In `db@1`, `user`, `room` and `applet` records belong to the applet and carry over every promotion; `attempt` records stay with their attempt. `storage@1` applet slots are still per version.
|
|
91
|
+
|
|
92
|
+
### Data across versions
|
|
93
|
+
|
|
94
|
+
When a new version changes a collection, the Rack validates the existing records against the new
|
|
95
|
+
schema at promotion and refuses to lose any silently: rejected records, a dropped collection that
|
|
96
|
+
holds records, or a lowered quota wait for an admin to confirm. A scope cannot change; use a new
|
|
97
|
+
collection name. To reshape existing records, ship a data migration in `migrations/`:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
// migrations/2026_10_09_120000_add_difficulty.js
|
|
101
|
+
export function up(db, ctx) { // db: { collection: [{ id, scope_id, owner_id, data, created_at, updated_at }] }
|
|
102
|
+
for (const q of db.questions) q.data.difficulty ??= 'medium';
|
|
103
|
+
ctx.log('back-filled difficulty');
|
|
104
|
+
}
|
|
105
|
+
export function down(db) { // runs if someone rolls back to a version without this file
|
|
106
|
+
for (const q of db.questions) delete q.data.difficulty;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Mutate `db` in place or return `{ collection: records[] }`; a returned collection replaces that
|
|
111
|
+
collection (missing records are deleted), others are kept. Migrations run in the browser of the
|
|
112
|
+
admin promoting, with no network: no imports, no `fetch`. The Rack rejects the whole result if any
|
|
113
|
+
record breaks the new schema or quota, changes `scope_id` or `owner_id`, or moves to a collection of
|
|
114
|
+
another scope. Keep every migration you ever ship; the Rack runs only those not yet applied. A
|
|
115
|
+
snapshot is taken before every cutover. `attempt` and preview records are never passed in.
|
|
87
116
|
|
|
88
117
|
## 4. The SDK
|
|
89
118
|
|
|
@@ -96,7 +125,7 @@ const launch = await Rack.init({ slug: 'my-applet' }); // mock mode when not i
|
|
|
96
125
|
| Namespace | Calls | Notes |
|
|
97
126
|
|---|---|---|
|
|
98
127
|
| `Rack` | `init()`, `on('launch'|'event')`, `mode`, `eject()` | `eject()` needs no capability |
|
|
99
|
-
| `Rack.auth` | `user()`, `attribute(key, fallback)` | Released profile from launch data |
|
|
128
|
+
| `Rack.auth` | `user()`, `attribute(key, fallback)`, `roles()`, `hasRole(name)`, `isGuest()` | Released profile from launch data (`auth@1`). Roles need no capability, and **nothing enforces them server-side**: use them to choose what to render, not to protect data |
|
|
100
129
|
| `Rack.attempt` | `status`, `state()`, `segment()`, `setState()`, `setSegment()`, `complete()` | State max 64 KB; `complete()` idempotent; after it, state and segment are refused with `completed` |
|
|
101
130
|
| `Rack.activity` | `answer(id, value)`, `progress(stepId)`, `statement({...})` | Verbs `^[a-z][a-z0-9_.:-]*$`; answers put the value in `result.response` |
|
|
102
131
|
| `Rack.db.collection(name)` | `create`, `get`, `update`, `delete`, `list`, `count` | Filters and sorting only on `indexed` paths; 100 per page |
|
|
@@ -106,7 +135,7 @@ const launch = await Rack.init({ slug: 'my-applet' }); // mock mode when not i
|
|
|
106
135
|
| `Rack.media` | `embedUrl(id)` | Rack-built player URL |
|
|
107
136
|
|
|
108
137
|
Every call resolves to `{ ok, data?, error?, message? }` and never throws. Error codes: `timeout`,
|
|
109
|
-
`persist_failed`, `undeclared_capability`, `unknown_type`, `validation`, `forbidden`, `completed`,
|
|
138
|
+
`disposed`, `persist_failed`, `undeclared_capability`, `unknown_type`, `validation`, `forbidden`, `completed`,
|
|
110
139
|
`quota_exceeded`, `budget_exceeded`, `not_found`, `conflict`. Do not block the participant on acks;
|
|
111
140
|
`await` only what you must confirm (for example the final answer before `complete()`).
|
|
112
141
|
|
|
@@ -116,10 +145,13 @@ Outside a Rack (`npm run dev`), or if the Rack does not answer within 3 s, the S
|
|
|
116
145
|
that reads `./manifest.json` so declared capabilities, activities, collections and prompts behave as
|
|
117
146
|
they will in production. State and records persist in `localStorage` under `ziprack-mock:{slug}:*`.
|
|
118
147
|
Rooms use `BroadcastChannel`, so two tabs of your dev server form a room. `Rack.mock.ai(promptId, fn)`
|
|
119
|
-
answers prompts yourself; `Rack.mock.resetAttempt()` replays a completed attempt.
|
|
148
|
+
answers prompts yourself; `Rack.mock.resetAttempt()` replays a completed attempt. The mock
|
|
149
|
+
participant is a `member`; add `?ziprack-guest` to the URL to launch as a guest, and
|
|
150
|
+
`?ziprack-roles=judge,applet-author` to hold those roles.
|
|
120
151
|
|
|
121
152
|
For tests, `import { FakeHost } from '@ziprack/sdk/testing'` and pass `host.transport()` to
|
|
122
153
|
`Rack.init({ transport })`; then inspect `host.statements`, `host.answers()`, `host.records()`.
|
|
154
|
+
`new FakeHost(manifest, { guest: true, roles: ['judge'] })` sets the identity.
|
|
123
155
|
|
|
124
156
|
## 5. Studio, and its MCP server
|
|
125
157
|
|
|
@@ -169,7 +201,9 @@ Uploads land unpublished. Promoting a version is a deliberate admin action. A ne
|
|
|
169
201
|
capability or a profile attribute waits for explicit approval; adding or removing `attempt@1` changes
|
|
170
202
|
the applet from rolling to pinned (or back) and is flagged prominently. Known vulnerabilities from your
|
|
171
203
|
SBOM appear in the review queue. Changing an activity `id` between versions splits export columns;
|
|
172
|
-
changing a `label` only changes headers.
|
|
204
|
+
changing a `label` only changes headers. Collection changes are checked against the live data at
|
|
205
|
+
promotion (see *Data across versions*); a version with migrations is promoted from the Rack's UI,
|
|
206
|
+
where they run.
|
|
173
207
|
|
|
174
208
|
## 8. Pre-upload checklist
|
|
175
209
|
|
|
@@ -178,5 +212,7 @@ changing a `label` only changes headers.
|
|
|
178
212
|
- [ ] Every question has an `activities[]` entry with a stable `id`, a `type`, a full-sentence `label`.
|
|
179
213
|
- [ ] `setState()` after every transition; refreshing mid-way in mock mode resumes.
|
|
180
214
|
- [ ] `complete()` exactly once; reloading a completed attempt shows the final screen.
|
|
215
|
+
- [ ] Changed a collection's schema? Existing records still validate, or a migration in `migrations/` reshapes them (with `down`).
|
|
216
|
+
- [ ] Every role-gated control also works for someone without the role, and nothing secret depends on a role.
|
|
181
217
|
- [ ] All URLs relative, no external `<script>`, no external images or APIs.
|
|
182
218
|
- [ ] Layout works at phone width and leaves the Rack bar alone.
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { HostBridge } from './core/bridge';
|
|
|
6
6
|
export type { AnswerOptions, StatementInput } from './activity';
|
|
7
7
|
export type { Collection, ListQuery, ListResult, RecordRow, WhereClause, WhereOperator } from './db';
|
|
8
8
|
export type { StoredFile, FileStatus } from './storage';
|
|
9
|
-
export type { Chat, ChatReply, Usage } from './ai';
|
|
9
|
+
export type { Chat, ChatReply, Citation, Usage } from './ai';
|
|
10
10
|
export type { Channel, RoomEventName, RoomEvents } from './broadcast';
|
|
11
11
|
export type { MockManifest, AiMockHandler } from './core/mock/MockRack';
|
|
12
12
|
/** The singleton every applet talks through. */
|