@ziprack/sdk 0.2.1 → 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.
@@ -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
 
@@ -125,6 +126,53 @@ instead of loosening the policy.
125
126
  - **Channel authentication.** The Rack signs its own WebSocket subscriptions. No token reaches the
126
127
  frame.
127
128
 
129
+ <a name="the-migration-runner"></a>
130
+ ## The Migration Runner
131
+
132
+ [Data migrations](database.md#data-migrations) are package code too, and they run outside the
133
+ applet frame: in the browser of the admin promoting the version, over the applet's whole dataset.
134
+ Two things contain them.
135
+
136
+ **The runner page has no network.** `GET /ziprack/migration-runner` is served with
137
+
138
+ ```
139
+ default-src 'none'; script-src 'unsafe-inline' blob:; style-src 'unsafe-inline';
140
+ connect-src 'none'; img-src 'none'; font-src 'none'; frame-src 'none'; worker-src blob:;
141
+ form-action 'none'; base-uri 'none'; frame-ancestors 'self'; sandbox allow-scripts
142
+ ```
143
+
144
+ and migration code never runs in its window. The page starts a dedicated worker from a `blob:` URL,
145
+ which inherits that CSP, and the worker imports each migration from a `blob:` URL and runs it. A
146
+ worker has no document, no forms and no writable `location`, so migration code cannot navigate a
147
+ frame or a window to carry the data out in a URL, which a CSP alone cannot stop. Under the CSP it
148
+ cannot fetch or open a socket either, and the worker also removes `fetch`, `XMLHttpRequest`,
149
+ `WebSocket`, `EventSource`, `WebTransport` and `importScripts` before it imports anything. Its only
150
+ way out is `postMessage` to the runner page, which relays to the page that framed it. A worker that
151
+ stops responding is terminated. The page holds no data until that parent posts some in, and is never
152
+ publishable.
153
+
154
+ **It always runs sandboxed.** The runner is served from the Rack's own origin, so its CSP carries
155
+ `sandbox allow-scripts`: the browser gives the page an opaque origin however it is framed, and
156
+ migration code cannot reach `window.parent`, the admin's session or the admin API. A Rack UI
157
+ should still frame it with `sandbox="allow-scripts"` (and never `allow-same-origin`), exactly as
158
+ it frames an applet, so the isolation does not rest on one header.
159
+
160
+ **Migrations are privileged.** A migration is handed every carried record of the applet, in every
161
+ `user`, `room` and `applet` scope, and can rewrite any of them. It is trusted the way the package's
162
+ server-side declarations are, not the way applet code is: promoting a version that runs migrations is
163
+ an admin's decision to run that code over everyone's data. The plan carries each migration's source
164
+ (`GET …/versions/{version}/cutover`), so it can be read before it runs.
165
+
166
+ **The server checks the shape of what comes back.** What the migrations return is checked before it
167
+ replaces any data, and again inside the promotion's transaction: the whole resulting dataset,
168
+ collections left out included, must match the target schema and quota; `scope_id` and `owner_id`
169
+ cannot change; a record moves only between collections of the same scope; a new `user` or `room`
170
+ record must name a scope that already has records, and no owner or one who already owns a record.
171
+ These keep every record where it was, but they cannot tell whose values a record holds: a migration
172
+ could copy or swap values between two participants' records and pass. What a migration cannot do is
173
+ send the data anywhere, reach another applet's data, or see `attempt` and preview records. And the
174
+ data it replaced is in the [snapshot](database.md#snapshots) taken before the cutover.
175
+
128
176
  <a name="threats-the-boundary-does-not-cover"></a>
129
177
  ## Threats the Boundary Does Not Cover
130
178
 
@@ -140,6 +188,11 @@ per month — never advisory numbers.
140
188
  **Record and storage flooding.** Per-scope quotas, declared in the manifest and capped by the
141
189
  deployment regardless of what the manifest asks for.
142
190
 
191
+ **Trusting roles.** An applet's [roles](auth.md#roles) arrive in the launch payload and are read by
192
+ applet code, which runs in the participant's browser. No capability enforces them server-side yet,
193
+ so a role is a reason to show a control, not a permission. Keep anything a role must protect behind
194
+ a scope, a `permissions` rule or a facilitator event, which the Rack does enforce.
195
+
143
196
  **Supply chain.** The SBOM is self-attested. OSV findings catch negligence; the
144
197
  [capability diff](publishing.md#capability-diff-review) is what catches the realistic attack, which
145
198
  is a benign v1 followed by a quietly wider v2.
@@ -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. |
@@ -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).
@@ -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` (per version, read-mostly).
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.js CHANGED
@@ -1,5 +1,5 @@
1
- import { n as f, c as h, i as g, a as u, M as l, b as w, L as b } from "./store-BXGon4iJ.js";
2
- import { F as K, P as V, d as W } from "./store-BXGon4iJ.js";
1
+ import { n as f, c as h, i as g, a as u, M as l, b as w, L as b } from "./store-DQbG_t2D.js";
2
+ import { F as K, P as V, d as W } from "./store-DQbG_t2D.js";
3
3
  class v {
4
4
  constructor(t) {
5
5
  this.link = t;
@@ -113,7 +113,7 @@ class L {
113
113
  return e.ok && (this.local.status = "completed"), e;
114
114
  }
115
115
  }
116
- class A {
116
+ class R {
117
117
  constructor(t) {
118
118
  this.link = t;
119
119
  }
@@ -125,8 +125,23 @@ class A {
125
125
  const s = this.user();
126
126
  return !s || !(t in s) ? e : s[t];
127
127
  }
128
+ /**
129
+ * The roles the participant holds: `member` or `guest`, `applet-author` and
130
+ * `rack-administrator` when they apply, and the manifest's own roles the
131
+ * Rack gives them. Read at launch; a change applies from the next launch.
132
+ */
133
+ roles() {
134
+ return this.link.launch()?.roles ?? [];
135
+ }
136
+ hasRole(t) {
137
+ return this.roles().includes(t);
138
+ }
139
+ /** Whether the participant is a guest rather than a signed-in Rack user. */
140
+ isGuest() {
141
+ return this.hasRole("guest");
142
+ }
128
143
  }
129
- const R = "$room";
144
+ const A = "$room";
130
145
  class E {
131
146
  constructor(t, e) {
132
147
  this.link = t, this.name = e, this.listeners = /* @__PURE__ */ new Map(), this.unsubscribe = null;
@@ -152,7 +167,7 @@ class E {
152
167
  return this.unsubscribe?.(), this.unsubscribe = null, this.listeners.clear(), this.link.send("broadcast:leave", { channel: this.name });
153
168
  }
154
169
  }
155
- class T {
170
+ class S {
156
171
  constructor(t) {
157
172
  this.link = t, this.channels = /* @__PURE__ */ new Map(), this.roomListeners = /* @__PURE__ */ new Map(), this.localRole = null, this.localMembers = null, this.localState = null, this.detach = null, this.state = {
158
173
  /** Server-arbitrated shared state. */
@@ -219,7 +234,7 @@ class T {
219
234
  }
220
235
  ensureRoomListener() {
221
236
  this.detach ??= this.link.onEvent((t) => {
222
- if (t.channel !== R)
237
+ if (t.channel !== A)
223
238
  return;
224
239
  const e = t.payload;
225
240
  t.event === "presence" && Array.isArray(e.members) && (this.localMembers = e.members), t.event === "state" && typeof e.key == "string" && (this.localState = { ...this.localState ?? this.launch()?.state ?? {}, [e.key]: e.value }), t.event === "role" && e.participant_id === this.link.launch()?.actor.id && (this.localRole = e.role), this.roomListeners.get(t.event)?.forEach((s) => s(e));
@@ -229,7 +244,7 @@ class T {
229
244
  function d(r) {
230
245
  return { ok: !1, error: "undeclared_capability", message: `The manifest does not declare the [${r}] capability.` };
231
246
  }
232
- class S {
247
+ class T {
233
248
  constructor(t, e) {
234
249
  this.link = t, this.name = e;
235
250
  }
@@ -261,7 +276,7 @@ class D {
261
276
  this.link = t;
262
277
  }
263
278
  collection(t) {
264
- return new S(this.link, t);
279
+ return new T(this.link, t);
265
280
  }
266
281
  collections() {
267
282
  return this.link.launch()?.db?.collections ?? [];
@@ -380,16 +395,16 @@ class P {
380
395
  return;
381
396
  }
382
397
  if (t.partial && !t.done) {
383
- const { requestId: F, ok: U, partial: $, ...k } = t;
398
+ const { requestId: F, ok: j, partial: $, ...k } = t;
384
399
  e.onPartial?.(k), clearTimeout(e.timer), e.timer = setTimeout(() => this.expire(t.requestId), e.timeoutMs);
385
400
  return;
386
401
  }
387
402
  clearTimeout(e.timer), this.pending.delete(t.requestId);
388
- const { requestId: s, ok: n, data: i, error: a, message: o, details: p, partial: C, done: j, ...c } = t;
403
+ const { requestId: s, ok: n, data: i, error: a, message: o, details: p, partial: U, done: z, ...c } = t;
389
404
  e.resolve({ ok: n, data: i ?? (Object.keys(c).length ? c : void 0), error: a, message: o, details: p });
390
405
  }
391
406
  }
392
- const q = 3e3, I = 5e3;
407
+ const I = 3e3, q = 5e3;
393
408
  class m {
394
409
  constructor() {
395
410
  this.transport = null, this.initPromise = null, this.launchData = null, this.queue = [], this.launchListeners = /* @__PURE__ */ new Set(), this.eventListeners = /* @__PURE__ */ new Set(), this.pendingAiMocks = [];
@@ -401,7 +416,7 @@ class m {
401
416
  onEvent: (e) => this.on("event", e),
402
417
  mode: () => this.mode
403
418
  };
404
- this.auth = new A(t), this.attempt = new L(t), this.activity = new v(t), this.db = new D(t), this.storage = new O(t), this.ai = new M(t), this.broadcast = new T(t), this.media = new _(t), this.mock = {
419
+ this.auth = new R(t), this.attempt = new L(t), this.activity = new v(t), this.db = new D(t), this.storage = new O(t), this.ai = new M(t), this.broadcast = new S(t), this.media = new _(t), this.mock = {
405
420
  ai: (e, s) => {
406
421
  const n = this.mockRack();
407
422
  n ? n.mockAi(e, s) : this.pendingAiMocks.push([e, s]);
@@ -464,7 +479,7 @@ class m {
464
479
  this.transport?.dispose(), this.transport = null, this.initPromise = null, this.launchData = null;
465
480
  }
466
481
  async performInit(t) {
467
- const e = t.ackTimeoutMs ?? I, s = t.launchTimeoutMs ?? q;
482
+ const e = t.ackTimeoutMs ?? q, s = t.launchTimeoutMs ?? I;
468
483
  if (t.transport) {
469
484
  const o = t.launch ?? (t.transport instanceof l ? t.transport.rack.launchData() : null);
470
485
  if (!o)
@@ -481,7 +496,8 @@ class m {
481
496
  async activateMock(t, e) {
482
497
  console.info(`[ziprack] running in mock mode (${e})`);
483
498
  const s = await x(t.manifestUrl ?? "./manifest.json", t.slug), n = new w(s, new b(), {
484
- log: (i, a) => console.info(`[ziprack mock] ${i}`, a)
499
+ log: (i, a) => console.info(`[ziprack mock] ${i}`, a),
500
+ ...C()
485
501
  });
486
502
  for (const [i, a] of this.pendingAiMocks)
487
503
  n.mockAi(i, a);
@@ -514,8 +530,15 @@ async function x(r, t) {
514
530
  return console.warn(`[ziprack mock] could not load ${r}; mock mode will accept every capability.`, e), null;
515
531
  }
516
532
  }
517
- const H = new m();
518
- function N() {
533
+ function C(r = typeof location > "u" ? "" : location.search) {
534
+ const t = new URLSearchParams(r), e = (t.get("ziprack-roles") ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
535
+ return {
536
+ ...e.length > 0 ? { roles: e } : {},
537
+ ...t.has("ziprack-guest") ? { guest: t.get("ziprack-guest") !== "0" } : {}
538
+ };
539
+ }
540
+ const N = new m();
541
+ function B() {
519
542
  return new m();
520
543
  }
521
544
  export {
@@ -523,8 +546,8 @@ export {
523
546
  P as HostBridge,
524
547
  V as PROTOCOL_NAMESPACE,
525
548
  W as PROTOCOL_VERSION,
526
- H as Rack,
549
+ N as Rack,
527
550
  m as RackClient,
528
- N as createRack
551
+ B as createRack
529
552
  };
530
553
  //# sourceMappingURL=index.js.map