@ziprack/sdk 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db/index.d.ts +2 -1
- package/dist/docs/README.md +56 -0
- package/dist/docs/activity.md +144 -0
- package/dist/docs/ai.md +159 -0
- package/dist/docs/applet-packages.md +118 -0
- package/dist/docs/attempts.md +187 -0
- package/dist/docs/auth.md +106 -0
- package/dist/docs/broadcast.md +226 -0
- package/dist/docs/conformance.md +102 -0
- package/dist/docs/database.md +230 -0
- package/dist/docs/extending.md +261 -0
- package/dist/docs/installation.md +186 -0
- package/dist/docs/introduction.md +124 -0
- package/dist/docs/manifest.md +210 -0
- package/dist/docs/mcp.md +190 -0
- package/dist/docs/media.md +85 -0
- package/dist/docs/protocol.md +121 -0
- package/dist/docs/publishing.md +127 -0
- package/dist/docs/reporting.md +190 -0
- package/dist/docs/sdk.md +258 -0
- package/dist/docs/security.md +153 -0
- package/dist/docs/storage.md +131 -0
- package/dist/docs/studio.md +425 -0
- package/dist/docs/versioning.md +101 -0
- package/dist/docs/ziprack-1-authoring.md +182 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/bin.d.ts +2 -0
- package/dist/mcp/bin.js +3 -0
- package/dist/mcp/docs.d.ts +31 -0
- package/dist/mcp/docs.js +107 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mcp/rpc.d.ts +33 -0
- package/dist/mcp/rpc.js +86 -0
- package/dist/mcp/server.d.ts +23 -0
- package/dist/mcp/server.js +55 -0
- package/dist/mcp/tools.d.ts +20 -0
- package/dist/mcp/tools.js +106 -0
- package/dist/ziprack-sdk.iife.js.map +1 -1
- package/package.json +10 -4
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# Writing a Capability
|
|
2
|
+
|
|
3
|
+
- [When to Add One](#when-to-add-one)
|
|
4
|
+
- [The Capability Interface](#the-capability-interface)
|
|
5
|
+
- [Registering It](#registering-it)
|
|
6
|
+
- [Manifest Rules](#manifest-rules)
|
|
7
|
+
- [Message Handlers](#message-handlers)
|
|
8
|
+
- [Launch Payload](#launch-payload)
|
|
9
|
+
- [Install Hooks](#install-hooks)
|
|
10
|
+
- [Conformance and Status](#conformance-and-status)
|
|
11
|
+
- [The SDK Side](#the-sdk-side)
|
|
12
|
+
|
|
13
|
+
<a name="when-to-add-one"></a>
|
|
14
|
+
## When to Add One
|
|
15
|
+
|
|
16
|
+
A capability is one applet-facing surface: a set of message types, a manifest section, a slice of the
|
|
17
|
+
launch payload, and the server-side service behind them. `ziprack/db` and `ziprack/ai` are both
|
|
18
|
+
exactly this shape.
|
|
19
|
+
|
|
20
|
+
Before adding one, apply the framework's own test:
|
|
21
|
+
|
|
22
|
+
> If a capability requires an applet author to reason about device identity, transport, or
|
|
23
|
+
> deployment topology, it does not belong in zipRACK.
|
|
24
|
+
|
|
25
|
+
And check it is not already expressible. A great many ideas reduce to `db@1` with a different
|
|
26
|
+
collection, or to `broadcast@1` with a different event name. The value of seven surfaces is that
|
|
27
|
+
there are seven.
|
|
28
|
+
|
|
29
|
+
<a name="the-capability-interface"></a>
|
|
30
|
+
## The Capability Interface
|
|
31
|
+
|
|
32
|
+
One package, one `Capability`. Extend `AbstractCapability` and override only what you need — every
|
|
33
|
+
method has a sensible no-op default.
|
|
34
|
+
|
|
35
|
+
```php
|
|
36
|
+
namespace ZipRack\Survey;
|
|
37
|
+
|
|
38
|
+
use ZipRack\Core\Capabilities\AbstractCapability;
|
|
39
|
+
|
|
40
|
+
class SurveyCapability extends AbstractCapability
|
|
41
|
+
{
|
|
42
|
+
public function id(): string
|
|
43
|
+
{
|
|
44
|
+
return 'survey'; // ^[a-z][a-z0-9_]*$
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public function versions(): array
|
|
48
|
+
{
|
|
49
|
+
return [1]; // may implement several at once: [1, 2]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Method | Purpose |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `id()` | The name in manifests and message types: `survey`, `survey:submit`. |
|
|
57
|
+
| `versions()` | Capability versions this package implements. Metadata, independent of package semver. |
|
|
58
|
+
| `manifestSections()` | Top-level manifest keys this capability owns. |
|
|
59
|
+
| `manifestRules()` | Laravel rules for those sections. |
|
|
60
|
+
| `validateManifest()` | Cross-field checks; add errors to the validator. |
|
|
61
|
+
| `messageHandlers()` | `type => handler class`. |
|
|
62
|
+
| `launchPayload()` | What to merge into `rack:launch` under this capability's id. |
|
|
63
|
+
| `onInstall()` / `onPromote()` / `onRetire()` | Version lifecycle hooks. `onInstall()` runs inside the install transaction, so row changes only. |
|
|
64
|
+
| `adminRoutes()` | Headless admin routes, registered only when `ziprack/admin` is installed. |
|
|
65
|
+
| `conformanceTests()` | Checks contributed to the conformance suite. |
|
|
66
|
+
| `statusChecks()` | Warnings for `ziprack:status` and `/ziprack/health`. |
|
|
67
|
+
|
|
68
|
+
<a name="registering-it"></a>
|
|
69
|
+
## Registering It
|
|
70
|
+
|
|
71
|
+
```php
|
|
72
|
+
class SurveyServiceProvider extends ServiceProvider
|
|
73
|
+
{
|
|
74
|
+
public function register(): void
|
|
75
|
+
{
|
|
76
|
+
$this->mergeConfigFrom(__DIR__.'/../config/ziprack-survey.php', 'ziprack-survey');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public function boot(): void
|
|
80
|
+
{
|
|
81
|
+
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
|
|
82
|
+
|
|
83
|
+
ZipRack::register($this->app->make(SurveyCapability::class));
|
|
84
|
+
|
|
85
|
+
if ($this->app->runningInConsole()) {
|
|
86
|
+
$this->publishes([
|
|
87
|
+
__DIR__.'/../config/ziprack-survey.php' => config_path('ziprack-survey.php'),
|
|
88
|
+
], 'ziprack-config');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Migrations load; they are never published. Config publishes; it is meant to diverge. That split is
|
|
95
|
+
[not negotiable](installation.md#publishable-and-non-publishable) — it is what keeps the fleet
|
|
96
|
+
uniform.
|
|
97
|
+
|
|
98
|
+
The registry is built from installed providers, so installing the package *is* declaring the
|
|
99
|
+
capability. There is no separate list to fall out of sync.
|
|
100
|
+
|
|
101
|
+
<a name="manifest-rules"></a>
|
|
102
|
+
## Manifest Rules
|
|
103
|
+
|
|
104
|
+
```php
|
|
105
|
+
public function manifestSections(): array
|
|
106
|
+
{
|
|
107
|
+
return ['surveys'];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public function manifestRules(): array
|
|
111
|
+
{
|
|
112
|
+
return [
|
|
113
|
+
'surveys' => ['required', 'array', 'min:1'],
|
|
114
|
+
'surveys.*.id' => ['required', 'string', 'max:100', 'regex:/^[a-z][a-z0-9_.-]*$/'],
|
|
115
|
+
'surveys.*.questions' => ['required', 'array', 'min:1'],
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public function validateManifest(array $data, Validator $validator): void
|
|
120
|
+
{
|
|
121
|
+
$ids = [];
|
|
122
|
+
foreach ($data['surveys'] ?? [] as $index => $survey) {
|
|
123
|
+
if (isset($ids[$survey['id'] ?? null])) {
|
|
124
|
+
$validator->errors()->add("surveys.{$index}.id", "Duplicate survey id [{$survey['id']}].");
|
|
125
|
+
}
|
|
126
|
+
$ids[$survey['id'] ?? ''] = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The core validator enforces the section/capability relationship for you: a manifest containing
|
|
132
|
+
`surveys` without declaring `survey@1` fails, and so does one declaring a section no installed
|
|
133
|
+
capability owns.
|
|
134
|
+
|
|
135
|
+
Three conventions worth keeping: uniqueness errors name the duplicate, a section that depends on
|
|
136
|
+
another capability says so explicitly (`"A room-scoped X requires the [broadcast] capability"`), and
|
|
137
|
+
anything with a quota takes its ceiling from config rather than hard-coding it.
|
|
138
|
+
|
|
139
|
+
<a name="message-handlers"></a>
|
|
140
|
+
## Message Handlers
|
|
141
|
+
|
|
142
|
+
```php
|
|
143
|
+
public function messageHandlers(): array
|
|
144
|
+
{
|
|
145
|
+
return ['survey:submit' => SubmitSurvey::class];
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```php
|
|
150
|
+
class SubmitSurvey implements MessageHandler
|
|
151
|
+
{
|
|
152
|
+
public function rules(MessageContext $context): array
|
|
153
|
+
{
|
|
154
|
+
return [
|
|
155
|
+
'survey' => ['required', 'string', 'max:100'],
|
|
156
|
+
'answers' => ['required', 'array'],
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public function handle(MessageContext $context, array $payload): Ack
|
|
161
|
+
{
|
|
162
|
+
$survey = SurveyManifest::of($context->manifest())->find($payload['survey'])
|
|
163
|
+
?? throw MessageException::notFound("Survey [{$payload['survey']}] is not declared.");
|
|
164
|
+
|
|
165
|
+
// $context->participant, $context->version, $context->attribute('attempt_id'|'room_id')
|
|
166
|
+
|
|
167
|
+
return Ack::ok(['id' => $id]);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The dispatcher has already checked that the manifest declares your capability at a version this Rack
|
|
173
|
+
implements, and has already validated the payload against `rules()` — a handler never sees an
|
|
174
|
+
invalid payload or an undeclared call.
|
|
175
|
+
|
|
176
|
+
Throw `MessageException::forbidden() / notFound() / quota() / completed() / conflict()` for expected
|
|
177
|
+
failures; anything else is reported and acks `persist_failed`. Never leak an internal message into
|
|
178
|
+
an ack.
|
|
179
|
+
|
|
180
|
+
Message types must match `^[a-z][a-z0-9_]*:[a-z][a-z0-9_.]*$` and are namespaced by your capability
|
|
181
|
+
id. Everything the applet may influence about scope comes from `MessageContext`, never from the
|
|
182
|
+
payload — that is how a room-scoped write cannot address another room.
|
|
183
|
+
|
|
184
|
+
<a name="launch-payload"></a>
|
|
185
|
+
## Launch Payload
|
|
186
|
+
|
|
187
|
+
```php
|
|
188
|
+
public function launchPayload(LaunchContext $context): ?array
|
|
189
|
+
{
|
|
190
|
+
return ['surveys' => SurveyManifest::of($context->manifest())->summaries()];
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Merged under your capability id, so the applet reads `launch.survey.surveys`. Return `null` to
|
|
195
|
+
contribute nothing. Use `$context->remember(key, fn)` to share a resolved model with your message
|
|
196
|
+
handlers rather than querying twice.
|
|
197
|
+
|
|
198
|
+
Send what the applet needs to render its first frame, and nothing more. Launch data crosses into an
|
|
199
|
+
untrusted frame: it is the one place where over-sharing is silent.
|
|
200
|
+
|
|
201
|
+
<a name="install-hooks"></a>
|
|
202
|
+
## Install Hooks
|
|
203
|
+
|
|
204
|
+
```php
|
|
205
|
+
public function onInstall(AppletVersion $version, ?AppletVersion $previous): void
|
|
206
|
+
{
|
|
207
|
+
$this->installer->install($version, $previous); // row changes only; no DDL
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`onInstall()` runs inside the install transaction, with the previous current version for comparison
|
|
212
|
+
— which is what lets `db@1` register, update and orphan collections as they change between
|
|
213
|
+
versions. If it throws, the whole installation rolls back and extracted files are removed.
|
|
214
|
+
|
|
215
|
+
Keep it to row changes. `CREATE`, `ALTER` and `DROP` commit any open transaction on MySQL and
|
|
216
|
+
MariaDB, which breaks the install and lets the conformance suite's data escape its rollback. If a
|
|
217
|
+
capability needs tables, ship them as migrations; the conformance suite fails any check that commits
|
|
218
|
+
its transaction.
|
|
219
|
+
|
|
220
|
+
<a name="conformance-and-status"></a>
|
|
221
|
+
## Conformance and Status
|
|
222
|
+
|
|
223
|
+
```php
|
|
224
|
+
public function conformanceTests(): array
|
|
225
|
+
{
|
|
226
|
+
return [SurveyAnswersAreImmutable::class];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
public function statusChecks(): array
|
|
230
|
+
{
|
|
231
|
+
return $this->configured() ? [] : [[
|
|
232
|
+
'level' => 'warning',
|
|
233
|
+
'message' => 'survey@1 has no store configured; submissions are discarded.',
|
|
234
|
+
]];
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
A conformance check is a class with `name()` and `run(Fixture $fixture)`, failing by throwing. Write
|
|
239
|
+
one per invariant the capability promises an applet author — not one per method.
|
|
240
|
+
|
|
241
|
+
<a name="the-sdk-side"></a>
|
|
242
|
+
## The SDK Side
|
|
243
|
+
|
|
244
|
+
A capability is only half-built until `@ziprack/sdk` can call it. Add a module under
|
|
245
|
+
`sdk/src/{capability}/`, take a `Link` in the constructor, and expose it on `RackClient`:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
export class SurveyApi {
|
|
249
|
+
public constructor(private readonly link: Link) {}
|
|
250
|
+
|
|
251
|
+
public submit(survey: string, answers: JsonObject): Promise<AckResult<{ id: string }>> {
|
|
252
|
+
return this.link.send('survey:submit', { survey, answers });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Then implement it in `MockRack` as well. **A capability with no mock is a capability applet authors
|
|
258
|
+
cannot develop against**, and standalone development is the property the framework most depends on —
|
|
259
|
+
losing it costs more than the capability is worth.
|
|
260
|
+
|
|
261
|
+
Bump the capability version only when this surface changes. Refactors below it are free.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
- [Composing a Rack](#composing-a-rack)
|
|
4
|
+
- [The Deployment Skeleton](#the-deployment-skeleton)
|
|
5
|
+
- [Configuration](#configuration)
|
|
6
|
+
- [Routes](#routes)
|
|
7
|
+
- [Participants and Guests](#participants-and-guests)
|
|
8
|
+
- [Publishable and Non-Publishable](#publishable-and-non-publishable)
|
|
9
|
+
- [Console Commands](#console-commands)
|
|
10
|
+
|
|
11
|
+
<a name="composing-a-rack"></a>
|
|
12
|
+
## Composing a Rack
|
|
13
|
+
|
|
14
|
+
**A deployment's `composer.json` is its capability declaration.** The capability registry is built
|
|
15
|
+
by iterating installed service providers, so it cannot disagree with what is actually installed.
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
"require": {
|
|
19
|
+
"ziprack/core": "^1.0",
|
|
20
|
+
"ziprack/auth": "^1.0",
|
|
21
|
+
"ziprack/attempt": "^1.0",
|
|
22
|
+
"ziprack/activity": "^1.0",
|
|
23
|
+
"ziprack/db": "^1.0",
|
|
24
|
+
"ziprack/ai": "^1.0",
|
|
25
|
+
"ziprack/broadcast": "^1.0",
|
|
26
|
+
"ziprack/storage": "^1.0",
|
|
27
|
+
"ziprack/admin": "^1.0"
|
|
28
|
+
},
|
|
29
|
+
"require-dev": {
|
|
30
|
+
"ziprack/conformance": "^1.0"
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Remove `ziprack/db` and an applet requesting `db@1` is refused at upload with a message naming what
|
|
35
|
+
this Rack does implement. Drop `ziprack/broadcast` and there is no Reverb dependency, no WebSocket
|
|
36
|
+
port and no room tables — a smaller attack surface, not just a smaller install.
|
|
37
|
+
|
|
38
|
+
`ziprack/core` is mandatory; every capability package requires it. `ziprack/admin` is optional and
|
|
39
|
+
headless: it ships JSON endpoints and export services, never pages. `ziprack/conformance` is a dev
|
|
40
|
+
dependency that belongs in CI.
|
|
41
|
+
|
|
42
|
+
Packages resolve from private VCS repositories:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
"repositories": [
|
|
46
|
+
{ "type": "vcs", "url": "git@github.com:Collective-Next/ziprack-core.git" },
|
|
47
|
+
{ "type": "vcs", "url": "git@github.com:Collective-Next/ziprack-attempt.git" }
|
|
48
|
+
]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Inside the monorepo they resolve from `../packages/*` path repositories instead, so changes are
|
|
52
|
+
testable without tagging.
|
|
53
|
+
|
|
54
|
+
<a name="the-deployment-skeleton"></a>
|
|
55
|
+
## The Deployment Skeleton
|
|
56
|
+
|
|
57
|
+
`skeleton/` is a Laravel application template cloned once per engagement and then owned by that
|
|
58
|
+
engagement. It holds what is *meant* to diverge: branding, environment, the deployment pipeline, the
|
|
59
|
+
admin UI, and the capability list.
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
cp .env.example .env && php artisan key:generate
|
|
63
|
+
php artisan migrate --seed
|
|
64
|
+
npm install && npm run build
|
|
65
|
+
php artisan ziprack:install path/to/applet-1.0.0.zip --activate
|
|
66
|
+
php artisan ziprack:conformance
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Migrations ship inside the packages and load through `loadMigrationsFrom()`; they are never
|
|
70
|
+
published, because schema must be identical on every Rack.
|
|
71
|
+
|
|
72
|
+
<a name="configuration"></a>
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
Each package publishes one config file. The defaults are production-safe; the values below are the
|
|
76
|
+
ones you will actually touch.
|
|
77
|
+
|
|
78
|
+
### `config/ziprack.php`
|
|
79
|
+
|
|
80
|
+
| Key | Default | Notes |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `disk` | `local` | Where packages are extracted. Must not be publicly served. |
|
|
83
|
+
| `max_upload_mb` | `50` | Package size cap. |
|
|
84
|
+
| `allowed_extensions` | static list | Anything else rejects the whole upload. |
|
|
85
|
+
| `asset_token_ttl_hours` | `6` | Lifetime of the signed package-asset token. |
|
|
86
|
+
| `session_ttl_hours` | `6` | Lifetime of a launch session. |
|
|
87
|
+
| `csp.img_hosts`, `csp.media_hosts` | `[]` | Per-deployment allowlist. A bare scheme is rejected. |
|
|
88
|
+
| `player.frame_allow` | `[]` | Features added to the applet iframe's `allow` list, e.g. `microphone`. Bare names from a fixed set only. |
|
|
89
|
+
| `media.providers` | `['vimeo','youtube']` | Drives `frame-src` and manifest validation. |
|
|
90
|
+
| `publishing.require_sbom` | `true` | Refuse packages with no SBOM. |
|
|
91
|
+
| `publishing.require_review_for_new_capabilities` | `true` | Block promotion until a widened `requires` is approved. |
|
|
92
|
+
| `publishing.osv_lookup` | `true` | Batch the SBOM's purls against OSV.dev after install. |
|
|
93
|
+
| `audit.enabled` | `true` | Per-applet audit log of capability calls. |
|
|
94
|
+
| `health_token` | `null` | Bearer token for `/ziprack/health`. |
|
|
95
|
+
|
|
96
|
+
### Capability configs
|
|
97
|
+
|
|
98
|
+
| File | Notable keys |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `ziprack-auth.php` | `releasable` — the single place a deployment answers "what can applets see about our people"; `claim_map` |
|
|
101
|
+
| `ziprack-attempt.php` | `max_state_bytes` (64 KB), `max_segment_keys` (20) |
|
|
102
|
+
| `ziprack-db.php` | `max_records_per_scope` (5000), `max_record_bytes` (64 KB), `max_indexed_paths` (8), `list_limit` (100) |
|
|
103
|
+
| `ziprack-storage.php` | `disk`, `max_bytes_cap` (50 MB), `url_ttl_minutes` (10), `scanner` (`null` \| `clamav`) |
|
|
104
|
+
| `ziprack-ai.php` | `provider`, `models.analysis`, `models.chat`, `allowed_models`, the whole `budget` block |
|
|
105
|
+
| `ziprack-broadcast.php` | `join_prefix`, `facilitator_grace_seconds` (60), `presence_window_seconds` (45), `max_event_bytes` |
|
|
106
|
+
| `ziprack-admin.php` | `prefix` (`ziprack/admin`), `middleware` (`web`, `auth`, `can:ziprack.use-admin`) |
|
|
107
|
+
|
|
108
|
+
Ceilings in these files are **hard**: a manifest asking for a larger quota or budget fails
|
|
109
|
+
validation. Defaults apply when a manifest declares nothing.
|
|
110
|
+
|
|
111
|
+
<a name="routes"></a>
|
|
112
|
+
## Routes
|
|
113
|
+
|
|
114
|
+
| Route | Purpose |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `GET /a/{applet}` | The player: chrome plus the sandboxed iframe. Middleware from `ziprack.routes.middleware.player`, default `['web','auth']`. |
|
|
117
|
+
| `GET /ziprack/pkg/{token}/{path}` | Package assets, streamed with the contract's headers and CSP. Token-authenticated, stateless. |
|
|
118
|
+
| `GET /ziprack/assets/{file}` | The shell bundles (`shell.js`, `shell-broadcast.js`, `sdk.js`). |
|
|
119
|
+
| `POST /ziprack/s/{session}/messages` | The one door every capability call goes through. Batched, max 50 per request. |
|
|
120
|
+
| `GET /ziprack/s/{session}/stream/{requestId}` | Server-sent events for streamed acks (`ai:chat`). |
|
|
121
|
+
| `POST /ziprack/s/{session}/files` | Multipart upload target for `storage:put` (`storage`). |
|
|
122
|
+
| `GET /ziprack/f/{file}` | Signed, short-lived download of a stored file (`storage`). |
|
|
123
|
+
| `POST /ziprack/s/{session}/broadcast/auth` | Rack-side WebSocket channel authentication (`broadcast`). |
|
|
124
|
+
| `GET /ziprack/health` | Fleet visibility: format, core version, capabilities, warnings. |
|
|
125
|
+
| `GET /r/{code}` | Join a room by code (`broadcast`). |
|
|
126
|
+
| `*` `/ziprack/admin/*` | The headless admin API (`ziprack/admin`). |
|
|
127
|
+
|
|
128
|
+
Session routes are deliberately stateless: token in the path, no session cookie, no CSRF, rate
|
|
129
|
+
limited by `ziprack.routes.session_rate_limit`.
|
|
130
|
+
|
|
131
|
+
<a name="participants-and-guests"></a>
|
|
132
|
+
## Participants and Guests
|
|
133
|
+
|
|
134
|
+
By default the player sits behind `auth` and each authenticated user maps to a `Participant`.
|
|
135
|
+
Light group activities often have no identity source, so a Rack may enable guests:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
ZIPRACK_GUESTS=true
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
and drop `auth` from the player middleware. An unauthenticated visitor then gets a cookie-keyed
|
|
142
|
+
participant, which is enough for `opt_in` and `first_in` rooms. `roster` rooms and any applet
|
|
143
|
+
declaring `auth@1` still need real users.
|
|
144
|
+
|
|
145
|
+
To resolve participants differently — SSO subject, an LTI launch, an external roster — bind
|
|
146
|
+
`ZipRack\Core\Contracts\ParticipantResolver` in a service provider.
|
|
147
|
+
|
|
148
|
+
<a name="publishable-and-non-publishable"></a>
|
|
149
|
+
## Publishable and Non-Publishable
|
|
150
|
+
|
|
151
|
+
Every publishable file is a place the fleet can drift, so the split is deliberate.
|
|
152
|
+
|
|
153
|
+
**Publishable — meant to diverge:** config files, the player chrome and holding views
|
|
154
|
+
(`resources/views/vendor/ziprack`), translations, notification templates.
|
|
155
|
+
|
|
156
|
+
**Never publishable — these are the contract:**
|
|
157
|
+
|
|
158
|
+
| Artifact | Why |
|
|
159
|
+
|---|---|
|
|
160
|
+
| Migrations | Schema must be identical everywhere. |
|
|
161
|
+
| `ManifestValidator` | An applet must validate the same way on every Rack, or portability is fiction. |
|
|
162
|
+
| `PackageInstaller` file rules | Traversal, symlink and extension checks are security code. |
|
|
163
|
+
| Bridge and envelope handling | The `event.source` trust check is the whole boundary. |
|
|
164
|
+
| `ContentSecurityPolicy` | Directives are fixed; only the host allowlist is configurable. |
|
|
165
|
+
| `CapabilityRegistry` and dispatch | The runtime rejection of undeclared calls. |
|
|
166
|
+
|
|
167
|
+
Where deployments genuinely need to vary, the variation is configuration the core reads — not code
|
|
168
|
+
the deployment owns. A Rack can add an approved image host; it cannot reintroduce `https:`.
|
|
169
|
+
|
|
170
|
+
<a name="console-commands"></a>
|
|
171
|
+
## Console Commands
|
|
172
|
+
|
|
173
|
+
| Command | Does |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `ziprack:install {zip} [--applet=] [--activate]` | Install a package as a new version. |
|
|
176
|
+
| `ziprack:promote {slug} {version} [--force]` | Make a version current. `--force` also approves a pending capability review. |
|
|
177
|
+
| `ziprack:retire {slug} {version}` | Kill switch: the version can no longer be launched. |
|
|
178
|
+
| `ziprack:status [--json]` | Format, core version, installed capabilities, warnings. |
|
|
179
|
+
| `ziprack:conformance [--json]` | Verify this Rack honours `ziprack/1`. |
|
|
180
|
+
| `ziprack:conformance:install` | Publish a test that runs the suite inside the deployment's own Pest run. |
|
|
181
|
+
| `ziprack:profile:set {email} {key} {value}` | Set one profile attribute. |
|
|
182
|
+
| `ziprack:roster:import {csv}` | Import profile attributes by email. |
|
|
183
|
+
| `ziprack:room:create {slug} [--name=] [--mode=] [--roster=]` | Create a room and print its join code. |
|
|
184
|
+
| `ziprack:rooms:tick` | Vacate and reassign dropped facilitators. **Schedule every minute.** |
|
|
185
|
+
| `ziprack:db:prune [--force]` | Drop tables of collections no current manifest declares. |
|
|
186
|
+
| `ziprack:response-analysis [--applet=] [--force]` | Regenerate admin AI summaries. **Schedule every few hours.** |
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Introduction
|
|
2
|
+
|
|
3
|
+
- [The Shape of a Rack](#the-shape-of-a-rack)
|
|
4
|
+
- [The Trust Boundary](#the-trust-boundary)
|
|
5
|
+
- [Objects](#objects)
|
|
6
|
+
- [Capabilities](#capabilities)
|
|
7
|
+
- [What zipRACK Is Not](#what-ziprack-is-not)
|
|
8
|
+
|
|
9
|
+
<a name="the-shape-of-a-rack"></a>
|
|
10
|
+
## The Shape of a Rack
|
|
11
|
+
|
|
12
|
+
A **Rack** is one deployed Laravel application. It authenticates people, stores every byte an
|
|
13
|
+
applet produces, makes every outbound call, holds the WebSocket, and renders the admin reporting.
|
|
14
|
+
|
|
15
|
+
An **applet** is a zip containing a static web app. It is uploaded, validated, extracted and served
|
|
16
|
+
inside a sandboxed iframe at `/a/{slug}`. It owns layout, branding, media, navigation and
|
|
17
|
+
interaction — the entire participant-facing experience — and nothing else.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
┌─ Rack (Laravel) ───────────────────────────────────────────┐
|
|
21
|
+
│ auth · attempts · records · files · prompts · rooms │
|
|
22
|
+
│ ┌─ chrome ────────────────────────────────────────────┐ │
|
|
23
|
+
│ │ ┌─ iframe (opaque origin, no allow-same-origin) ──┐ │ │
|
|
24
|
+
│ │ │ applet: index.html + assets │ │ │
|
|
25
|
+
│ │ │ @ziprack/sdk ⇄ postMessage ⇄ shell │ │ │
|
|
26
|
+
│ │ └─────────────────────────────────────────────────┘ │ │
|
|
27
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
28
|
+
└────────────────────────────────────────────────────────────┘
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Every deployment is standalone: its own database, its own domain, its own capability set. There is
|
|
32
|
+
no shared multi-tenant monolith. What unites deployments is the format string `ziprack/1` and the
|
|
33
|
+
set of capability versions each Rack implements — which is why an applet built against one Rack runs
|
|
34
|
+
on another.
|
|
35
|
+
|
|
36
|
+
<a name="the-trust-boundary"></a>
|
|
37
|
+
## The Trust Boundary
|
|
38
|
+
|
|
39
|
+
The iframe is rendered with:
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<iframe sandbox="allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
|
|
43
|
+
allow="autoplay; fullscreen; picture-in-picture; encrypted-media">
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Note the absence of `allow-same-origin`. The applet document has an **opaque origin**: no cookies,
|
|
47
|
+
no `localStorage`, no `IndexedDB`, no credentialed requests, no way to call the Rack over HTTP.
|
|
48
|
+
Everything an applet needs, it asks for over `postMessage`:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
await Rack.activity.answer('intro.connection', 5);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
That call becomes a `ziprack` envelope posted to the parent window, which the shell batches into a
|
|
55
|
+
`POST /ziprack/s/{session}/messages`, which `MessageDispatcher` checks against the applet's manifest
|
|
56
|
+
before handing to the capability's handler. The applet asks; the Rack decides, enforces and persists.
|
|
57
|
+
|
|
58
|
+
Trust runs on `event.source`, never `event.origin` — the applet's origin is opaque by design, so
|
|
59
|
+
origin comparison is meaningless. A nested frame (a Vimeo player, say) that tries to talk to the
|
|
60
|
+
Rack is ignored, because its window is not the applet's `contentWindow`.
|
|
61
|
+
|
|
62
|
+
**This is why new capabilities are cheap.** Adding an LLM surface does not loosen the CSP, because
|
|
63
|
+
the call never leaves the frame as HTTP — it leaves as a `postMessage` the Rack fulfils server-side.
|
|
64
|
+
An applet can never obtain an API key, a channel token or a database handle, because none of those
|
|
65
|
+
ever cross the boundary.
|
|
66
|
+
|
|
67
|
+
<a name="objects"></a>
|
|
68
|
+
## Objects
|
|
69
|
+
|
|
70
|
+
| Object | What it is | Capability |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| **Applet** | One experience or app, identified by an immutable `slug`. Has many versions. | core |
|
|
73
|
+
| **Version** | One uploaded zip. Immutable. Exactly one is current. | core |
|
|
74
|
+
| **Participant** | The person in front of the applet, resolved from the Rack's own users (or a guest cookie). | core |
|
|
75
|
+
| **Attempt** | One participant's run. Holds `status`, `state`, `segment`. Pinned to the version it started on. | `attempt@1` |
|
|
76
|
+
| **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
|
+
| **Stored file** | A participant upload, addressable only once scanned clean. | `storage@1` |
|
|
79
|
+
| **Room** | A shared session: an applet version plus its members, each `participant` or `facilitator`. | `broadcast@1` |
|
|
80
|
+
| **Profile attribute** | A Rack-owned fact about the participant, released only if requested and permitted. | `auth@1` |
|
|
81
|
+
|
|
82
|
+
Statements and records coexist because they answer different questions. A statement is *what
|
|
83
|
+
happened*; a record is *what is currently true*. Statements are append-only and uniformly shaped, so
|
|
84
|
+
they export to one CSV per applet. Records are mutable and arbitrarily shaped, which is why `db@1`
|
|
85
|
+
requires a JSON Schema — the schema is what buys back the column headers.
|
|
86
|
+
|
|
87
|
+
<a name="capabilities"></a>
|
|
88
|
+
## Capabilities
|
|
89
|
+
|
|
90
|
+
Seven surfaces. An applet declares the ones it uses in its manifest, and the Rack refuses at runtime
|
|
91
|
+
any call the manifest did not declare.
|
|
92
|
+
|
|
93
|
+
| Capability | Covers | Entry point |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| [`auth@1`](auth.md) | Who the participant is; released profile attributes | `Rack.auth.user()` |
|
|
96
|
+
| [`attempt@1`](attempts.md) | Lifecycle, resume state, completion, version pinning | `Rack.attempt.complete()` |
|
|
97
|
+
| [`activity@1`](activity.md) | Append-only statements and the CSV export | `Rack.activity.answer(id, value)` |
|
|
98
|
+
| [`db@1`](database.md) | CRUD records in declared collections | `Rack.db.collection('ideas').create({…})` |
|
|
99
|
+
| [`storage@1`](storage.md) | Participant file uploads | `Rack.storage.put('photos', file)` |
|
|
100
|
+
| [`ai@1`](ai.md) | Declared prompt templates: analysis and chat | `Rack.ai.ask('summarize', vars)` |
|
|
101
|
+
| [`broadcast@1`](broadcast.md) | Rooms, presence, facilitator control, shared state | `Rack.broadcast.join('main')` |
|
|
102
|
+
|
|
103
|
+
Capability versions are independent of the format string and of the Composer package version. See
|
|
104
|
+
[Versioning](versioning.md).
|
|
105
|
+
|
|
106
|
+
Two things need no capability: [declared media](media.md) (`Rack.media.embedUrl(id)`) and
|
|
107
|
+
`Rack.eject()`, which asks the Rack to leave the applet.
|
|
108
|
+
|
|
109
|
+
<a name="what-ziprack-is-not"></a>
|
|
110
|
+
## What zipRACK Is Not
|
|
111
|
+
|
|
112
|
+
These were considered and settled. Reopening them needs new information, not a fresh opinion.
|
|
113
|
+
|
|
114
|
+
- **Not multi-tenant.** Standalone Racks per engagement. Isolation is infrastructure, not
|
|
115
|
+
application code. Say *deployment*, *Rack* or *fleet* — never *tenant*.
|
|
116
|
+
- **Not two standards.** There is no "experience profile" and "app profile". An experience is an
|
|
117
|
+
applet declaring `attempt@1` and `activity@1`; an app is one declaring `db@1` and not `attempt@1`.
|
|
118
|
+
- **No CRDTs in v1.** Shared room state is server-authoritative, last write wins per key.
|
|
119
|
+
- **No static analysis of applet JS.** Minified bundles defeat it (`window["ev"+"al"]`); the CSP
|
|
120
|
+
does that work at runtime where it cannot be evaded.
|
|
121
|
+
- **No display or kiosk role.** Rooms have participants and facilitators. Multi-screen AV work stays
|
|
122
|
+
bespoke. The general test: *if a capability requires an applet author to reason about device
|
|
123
|
+
identity, transport or deployment topology, it does not belong in zipRACK.*
|
|
124
|
+
- **No offline mode.** Participants are in offices with reliable connectivity.
|