@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/ai/index.d.ts
CHANGED
|
@@ -4,15 +4,34 @@ export interface Usage {
|
|
|
4
4
|
input_tokens: number;
|
|
5
5
|
output_tokens: number;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Where a grounded answer's evidence came from: a passage in one of the
|
|
9
|
+
* manifest's knowledge slots. Present only on prompts bound to knowledge, and
|
|
10
|
+
* empty when nothing relevant was found or retrieval was unavailable.
|
|
11
|
+
*/
|
|
12
|
+
export interface Citation {
|
|
13
|
+
slot: string;
|
|
14
|
+
title: string | null;
|
|
15
|
+
ref: string | null;
|
|
16
|
+
sources: Array<{
|
|
17
|
+
title: string;
|
|
18
|
+
ref?: string;
|
|
19
|
+
}>;
|
|
20
|
+
url: string | null;
|
|
21
|
+
t_start: number | null;
|
|
22
|
+
t_end: number | null;
|
|
23
|
+
}
|
|
7
24
|
export interface ChatReply {
|
|
8
25
|
message_id: string;
|
|
9
26
|
text: string;
|
|
10
27
|
usage: Usage;
|
|
28
|
+
citations?: Citation[];
|
|
11
29
|
}
|
|
12
30
|
export declare class Chat {
|
|
13
31
|
private readonly link;
|
|
14
32
|
readonly promptId: string;
|
|
15
|
-
|
|
33
|
+
private readonly vars;
|
|
34
|
+
constructor(link: Link, promptId: string, vars?: JsonObject);
|
|
16
35
|
/**
|
|
17
36
|
* Send a participant message. onDelta receives text as it streams; the
|
|
18
37
|
* resolved result carries the full text.
|
|
@@ -30,12 +49,25 @@ export declare class AiApi {
|
|
|
30
49
|
prompts(): Array<{
|
|
31
50
|
id: string;
|
|
32
51
|
mode: 'analysis' | 'chat';
|
|
52
|
+
knowledge?: string[];
|
|
53
|
+
}>;
|
|
54
|
+
/** The knowledge slots the manifest declares. Their contents stay on the Rack. */
|
|
55
|
+
knowledge(): Array<{
|
|
56
|
+
id: string;
|
|
57
|
+
label: string | null;
|
|
33
58
|
}>;
|
|
34
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Analysis prompt: structured JSON conforming to the declared output_schema.
|
|
61
|
+
* A prompt bound to knowledge also returns the citations it was grounded in.
|
|
62
|
+
*/
|
|
35
63
|
ask<T extends object = JsonObject>(promptId: string, vars?: JsonObject): Promise<AckResult<{
|
|
36
64
|
output: T;
|
|
37
65
|
usage: Usage;
|
|
66
|
+
citations?: Citation[];
|
|
38
67
|
}>>;
|
|
39
|
-
/**
|
|
40
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Chat prompt: conversation history is held by the Rack. `vars` fill the
|
|
70
|
+
* template's placeholders on every turn, e.g. which persona to speak as.
|
|
71
|
+
*/
|
|
72
|
+
chat(promptId: string, vars?: JsonObject): Chat;
|
|
41
73
|
}
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { LaunchUser } from '../core/types';
|
|
|
4
4
|
* auth@1 — who the participant is and the profile attributes the Rack chose to
|
|
5
5
|
* release. Attributes the manifest requested but the Rack withheld are simply
|
|
6
6
|
* absent; degrade gracefully.
|
|
7
|
+
*
|
|
8
|
+
* Roles need no capability: every launch carries them.
|
|
7
9
|
*/
|
|
8
10
|
export declare class AuthApi {
|
|
9
11
|
private readonly link;
|
|
@@ -11,4 +13,13 @@ export declare class AuthApi {
|
|
|
11
13
|
user(): LaunchUser | null;
|
|
12
14
|
/** A released profile attribute, or the fallback when withheld. */
|
|
13
15
|
attribute<T = unknown>(key: string, fallback?: T | null): T | null;
|
|
16
|
+
/**
|
|
17
|
+
* The roles the participant holds: `member` or `guest`, `applet-author` and
|
|
18
|
+
* `rack-administrator` when they apply, and the manifest's own roles the
|
|
19
|
+
* Rack gives them. Read at launch; a change applies from the next launch.
|
|
20
|
+
*/
|
|
21
|
+
roles(): string[];
|
|
22
|
+
hasRole(role: string): boolean;
|
|
23
|
+
/** Whether the participant is a guest rather than a signed-in Rack user. */
|
|
24
|
+
isGuest(): boolean;
|
|
14
25
|
}
|
|
@@ -39,12 +39,25 @@ export interface MockManifest {
|
|
|
39
39
|
id: string;
|
|
40
40
|
mode: 'analysis' | 'chat';
|
|
41
41
|
output_schema?: JsonObject;
|
|
42
|
+
knowledge?: Array<{
|
|
43
|
+
slot: string;
|
|
44
|
+
}>;
|
|
45
|
+
}>;
|
|
46
|
+
knowledge?: Array<{
|
|
47
|
+
id: string;
|
|
48
|
+
label?: string;
|
|
42
49
|
}>;
|
|
43
50
|
broadcast?: {
|
|
44
51
|
facilitator_events?: string[];
|
|
45
52
|
channels?: string[];
|
|
46
53
|
mock_mode?: 'first_in' | 'opt_in';
|
|
47
54
|
};
|
|
55
|
+
roles?: Array<{
|
|
56
|
+
name: string;
|
|
57
|
+
label?: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
grant?: 'everyone' | 'members' | 'guests' | 'assigned';
|
|
60
|
+
}>;
|
|
48
61
|
[key: string]: unknown;
|
|
49
62
|
}
|
|
50
63
|
export type AiMockHandler = (vars: JsonObject, mode: 'analysis' | 'chat', message?: string) => unknown | Promise<unknown>;
|
|
@@ -56,6 +69,10 @@ export interface MockRackOptions {
|
|
|
56
69
|
participantName?: string;
|
|
57
70
|
/** Force the mock room role instead of deriving it from the assignment mode. */
|
|
58
71
|
role?: RoomRole;
|
|
72
|
+
/** Launch as a guest: `guest` instead of `member`, and the guest grants. */
|
|
73
|
+
guest?: boolean;
|
|
74
|
+
/** Roles to hold on top of the grants, as if the Rack had assigned them (any name, built-in ones included). */
|
|
75
|
+
roles?: string[];
|
|
59
76
|
}
|
|
60
77
|
export type MockOutcome = AckResult | {
|
|
61
78
|
stream: AsyncIterable<JsonObject>;
|
|
@@ -93,6 +110,11 @@ export declare class MockRack {
|
|
|
93
110
|
onEvent(listener: (event: RackEvent) => void): () => void;
|
|
94
111
|
pushEvent(event: RackEvent): void;
|
|
95
112
|
launchData(nameHint?: string): LaunchData;
|
|
113
|
+
/**
|
|
114
|
+
* What a Rack would resolve: member or guest, the manifest's grants, and
|
|
115
|
+
* any roles the options say were assigned.
|
|
116
|
+
*/
|
|
117
|
+
roles(): string[];
|
|
96
118
|
mockAi(promptId: string, handler: AiMockHandler): void;
|
|
97
119
|
dispose(): void;
|
|
98
120
|
handle(type: string, payload: JsonObject): Promise<MockOutcome>;
|
package/dist/core/rack.d.ts
CHANGED
|
@@ -66,3 +66,12 @@ export declare class RackClient {
|
|
|
66
66
|
private flushQueue;
|
|
67
67
|
private mockRack;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Mock-mode identity from the page URL, so an author can try each role:
|
|
71
|
+
* `?ziprack-roles=dungeon-master,applet-author` adds assigned roles and
|
|
72
|
+
* `?ziprack-guest` launches as a guest.
|
|
73
|
+
*/
|
|
74
|
+
export declare function mockIdentityFromUrl(search?: string): {
|
|
75
|
+
roles?: string[];
|
|
76
|
+
guest?: boolean;
|
|
77
|
+
};
|
package/dist/core/types.d.ts
CHANGED
|
@@ -86,8 +86,15 @@ export interface LaunchData {
|
|
|
86
86
|
applet: LaunchApplet;
|
|
87
87
|
capabilities: string[];
|
|
88
88
|
media: Record<string, MediaEmbed>;
|
|
89
|
+
/**
|
|
90
|
+
* The roles the participant holds in this applet: `member` or `guest`,
|
|
91
|
+
* `applet-author` and `rack-administrator` when they apply, plus the
|
|
92
|
+
* manifest's own roles the Rack gives them. Absent on Racks before v1.8.
|
|
93
|
+
*/
|
|
94
|
+
roles?: string[];
|
|
89
95
|
auth?: {
|
|
90
96
|
user: LaunchUser;
|
|
97
|
+
withheld?: string[];
|
|
91
98
|
};
|
|
92
99
|
attempt?: LaunchAttempt;
|
|
93
100
|
activity?: {
|
|
@@ -107,10 +114,16 @@ export interface LaunchData {
|
|
|
107
114
|
max_bytes: number;
|
|
108
115
|
}>;
|
|
109
116
|
};
|
|
117
|
+
/** `knowledge` lists the slots a prompt is grounded in (Racks with knowledge support). */
|
|
110
118
|
ai?: {
|
|
111
119
|
prompts: Array<{
|
|
112
120
|
id: string;
|
|
113
121
|
mode: 'analysis' | 'chat';
|
|
122
|
+
knowledge?: string[];
|
|
123
|
+
}>;
|
|
124
|
+
knowledge?: Array<{
|
|
125
|
+
id: string;
|
|
126
|
+
label: string | null;
|
|
114
127
|
}>;
|
|
115
128
|
};
|
|
116
129
|
broadcast?: LaunchBroadcast;
|
package/dist/docs/README.md
CHANGED
|
@@ -24,10 +24,10 @@ These docs cover both halves. If you are building an applet, start with
|
|
|
24
24
|
|
|
25
25
|
## Capabilities
|
|
26
26
|
|
|
27
|
-
- [Authentication](auth.md) — `auth@1
|
|
27
|
+
- [Authentication](auth.md) — `auth@1`, and roles
|
|
28
28
|
- [Attempts](attempts.md) — `attempt@1`
|
|
29
29
|
- [Activity](activity.md) — `activity@1`
|
|
30
|
-
- [Database](database.md) — `db@1
|
|
30
|
+
- [Database](database.md) — `db@1`, data across versions, migrations, snapshots
|
|
31
31
|
- [Storage](storage.md) — `storage@1`
|
|
32
32
|
- [AI](ai.md) — `ai@1`
|
|
33
33
|
- [Broadcast](broadcast.md) — `broadcast@1`
|
|
@@ -37,7 +37,7 @@ These docs cover both halves. If you are building an applet, start with
|
|
|
37
37
|
|
|
38
38
|
- [Installation](installation.md) — composing a Rack from capability packages
|
|
39
39
|
- [Security](security.md) — the sandbox, the CSP, the threats it does not cover
|
|
40
|
-
- [Publishing](publishing.md) — install, capability diff, review queue, promotion, kill switch
|
|
40
|
+
- [Publishing](publishing.md) — install, capability diff, review queue, promotion and the data cutover, kill switch
|
|
41
41
|
- [Reporting](reporting.md) — the admin API, CSV export, metrics
|
|
42
42
|
- [Who may do what](reporting.md#who-may-do-what) — `AppletAccess`, abilities, applet owners
|
|
43
43
|
- [Conformance](conformance.md) — proving a deployment is still a Rack
|
package/dist/docs/ai.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
- [Chat](#chat)
|
|
7
7
|
- [Budgets](#budgets)
|
|
8
8
|
- [Untrusted Variables](#untrusted-variables)
|
|
9
|
+
- [Knowledge](#knowledge)
|
|
9
10
|
- [Not This Capability](#not-this-capability)
|
|
10
11
|
|
|
11
12
|
<a name="introduction"></a>
|
|
@@ -150,6 +151,89 @@ pass through, and design the template so that it would not matter:
|
|
|
150
151
|
schema-constrained answer is a much smaller blast radius.
|
|
151
152
|
- Show model output as model output. Do not render it as if it were Rack-authored guidance.
|
|
152
153
|
|
|
154
|
+
<a name="knowledge"></a>
|
|
155
|
+
## Knowledge
|
|
156
|
+
|
|
157
|
+
A prompt can be **grounded** in documents the Rack holds: before the model is called, the Rack
|
|
158
|
+
retrieves the most relevant passages and hands them over as quoted reference material. The applet
|
|
159
|
+
still sends only variables — it never sees the documents, the search or the filter.
|
|
160
|
+
|
|
161
|
+
Declare **slots** (named collections the Rack fills) and bind prompts to them:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
"knowledge": [
|
|
165
|
+
{
|
|
166
|
+
"id": "factbook",
|
|
167
|
+
"label": "Fact Book",
|
|
168
|
+
"fields": {
|
|
169
|
+
"stage": { "enum": ["s3_misdiagnosis", "s5_initial_treatment"] },
|
|
170
|
+
"stakeholders": { "type": "array", "items": { "enum": ["patient", "caregiver", "hepatologist"] } },
|
|
171
|
+
"persona_eligible": { "type": "boolean" }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"prompts": [
|
|
176
|
+
{
|
|
177
|
+
"id": "persona_chat",
|
|
178
|
+
"mode": "chat",
|
|
179
|
+
"template": "You are a composite {{persona}} from the fact book...",
|
|
180
|
+
"vars": { "type": "object", "properties": { "persona": { "enum": ["patient_neuro", "caregiver_parent"] } } },
|
|
181
|
+
"knowledge": [{
|
|
182
|
+
"slot": "factbook",
|
|
183
|
+
"top_k": 6,
|
|
184
|
+
"filter": { "persona_eligible": true },
|
|
185
|
+
"filter_by": { "persona": {
|
|
186
|
+
"patient_neuro": { "stakeholders": { "contains": "patient" } },
|
|
187
|
+
"caregiver_parent": { "stakeholders": { "contains": "caregiver" } }
|
|
188
|
+
} }
|
|
189
|
+
}]
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
| Field | Rules |
|
|
195
|
+
|---|---|
|
|
196
|
+
| `knowledge[].id` | Required. `^[a-z][a-z0-9_]*$`, unique. |
|
|
197
|
+
| `knowledge[].source` | `rack` (the default): documents are loaded after install, from the Rack or over MCP. |
|
|
198
|
+
| `knowledge[].fields` | The metadata a document may tag and a filter may use: `type` (`string`, `number`, `boolean`, `array`) and an optional `enum` (`items.enum` for arrays). `sensitivity`, `content`, `embedding`, `text` and `id` are reserved. |
|
|
199
|
+
| `prompts[].knowledge[].slot` | A declared slot. Up to three per prompt. |
|
|
200
|
+
| `top_k` | Passages to retrieve, up to the Rack's ceiling. |
|
|
201
|
+
| `query` | Analysis prompts: what to search for, with `{{var}}` placeholders. Default: the string variables. Chat prompts search with the participant's message. |
|
|
202
|
+
| `filter` | Fixed conditions: a value (equals), `{ "in": [...] }`, or `{ "contains": value }` for an array field. |
|
|
203
|
+
| `filter_by` | Maps each value of an **enum** variable to more conditions. Every enum value must be mapped. This is how one prompt serves several personas without the applet writing filters. |
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
const coach = Rack.ai.chat('persona_chat', { persona: 'caregiver_parent' });
|
|
207
|
+
const reply = await coach.send('What was the hardest moment?');
|
|
208
|
+
|
|
209
|
+
reply.data?.citations; // [{ slot, title, ref: 'slide 105', sources: [...], url, t_start, t_end }]
|
|
210
|
+
Rack.ai.knowledge(); // [{ id: 'factbook', label: 'Fact Book' }]
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Citations** come back on every grounded call — empty when nothing relevant was found or the
|
|
214
|
+
Rack's index was unavailable, in which case the model answered ungrounded. Show them; they are how a
|
|
215
|
+
participant can check an answer.
|
|
216
|
+
|
|
217
|
+
**Loading documents** happens on the Rack, not in the package: an author uploads to a slot from the
|
|
218
|
+
applet's AI tab or with the `create-knowledge-upload` MCP tool. The Rack accepts prepared JSONL (one
|
|
219
|
+
chunk per line: `id`, `text`, and optionally `context`, `title`, `ref`, `sources`, `url`, `t_start`,
|
|
220
|
+
`t_end`, `sensitivity`, `metadata` using the slot's fields), Markdown, plain text, HTML, and WebVTT or
|
|
221
|
+
SRT transcripts. Heavy sources — slide decks, PDFs, video — are prepared locally and arrive as JSONL
|
|
222
|
+
or a transcript; the Rack never parses them. The `get-knowledge-prep-skill` MCP tool installs an
|
|
223
|
+
agent skill that does that preparation, validation and loading. Chunks above the Rack's sensitivity ceiling are refused
|
|
224
|
+
and never leave the Rack, and each applet can be held to a lower ceiling for retrieval.
|
|
225
|
+
|
|
226
|
+
A Rack without a knowledge-capable ai package refuses a manifest with a `knowledge` section at
|
|
227
|
+
install, naming the unowned section, rather than answering ungrounded.
|
|
228
|
+
|
|
229
|
+
### Untrusted Passages
|
|
230
|
+
|
|
231
|
+
Retrieved text is document content, not Rack instruction. It travels in a quoted `<reference>` block
|
|
232
|
+
of the user message — never the system prompt — and the model is told to use it as evidence and never
|
|
233
|
+
to follow instructions inside it. Delimiting reduces indirect prompt injection; it does not eliminate
|
|
234
|
+
it. Load only documents you would be comfortable having quoted back to a participant, and use
|
|
235
|
+
`sensitivity` to keep anything that must not be quoted out of the index altogether.
|
|
236
|
+
|
|
153
237
|
<a name="not-this-capability"></a>
|
|
154
238
|
## Not This Capability
|
|
155
239
|
|
|
@@ -17,10 +17,16 @@ An applet ships as a single zip, named for the applet and the version it contain
|
|
|
17
17
|
├── manifest.json required, at the zip root
|
|
18
18
|
├── sbom.json required — CycloneDX, or package-lock.json
|
|
19
19
|
├── index.html the entry (configurable via manifest.entry)
|
|
20
|
+
├── migrations/… optional — data migrations, one ES module per file
|
|
20
21
|
└── assets/… everything the entry references, by RELATIVE path
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
`migrations/` is the one other directory the Rack reads: the [data migrations](database.md#data-migrations)
|
|
25
|
+
that reshape existing records when the version is promoted. Its files are ordinary `.js` or `.mjs`
|
|
26
|
+
files to the installer, named `YYYY_MM_DD_HHMMSS_name.js` and sitting directly inside it; any other
|
|
27
|
+
name there refuses the package.
|
|
28
|
+
|
|
29
|
+
Apart from those, nothing is special. The Rack extracts the archive, keeps the file tree intact, and serves it
|
|
24
30
|
through a token-prefixed path. There is no build step on the server, no server-side templating, no
|
|
25
31
|
framework to satisfy — whatever your bundler emits is what runs.
|
|
26
32
|
|
|
@@ -39,10 +45,11 @@ than skipping a bad file:
|
|
|
39
45
|
- **The slug must match** the applet you are uploading to, and it is immutable once created.
|
|
40
46
|
- **A `version` string uploads once per applet.** Bump it every time, including during testing.
|
|
41
47
|
|
|
42
|
-
Installation runs inside a transaction: the version row is created,
|
|
43
|
-
`onInstall()` hook runs — this is where `db@1`
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
Installation runs inside a transaction: the version row is created, the files are extracted, and
|
|
49
|
+
then every declared capability's `onInstall()` hook runs — this is where `db@1` checks seed files
|
|
50
|
+
and migration names and registers new collection names. No tables are created; records are only
|
|
51
|
+
touched at [promotion](publishing.md#the-data-cutover). A failure anywhere rolls the whole thing
|
|
52
|
+
back and deletes the extracted directory.
|
|
46
53
|
|
|
47
54
|
<a name="the-starter"></a>
|
|
48
55
|
## The Starter
|
|
@@ -97,15 +104,18 @@ having, because negligence is the common case.
|
|
|
97
104
|
`npm run package` in the starter does the whole job:
|
|
98
105
|
|
|
99
106
|
```js
|
|
100
|
-
// scripts/package.
|
|
107
|
+
// scripts/package.mjs — abbreviated
|
|
101
108
|
const manifest = JSON.parse(readFileSync('dist/manifest.json', 'utf8'));
|
|
102
109
|
|
|
103
110
|
execSync('npm sbom --sbom-format cyclonedx --omit dev', { cwd: lockDir }) // → dist/sbom.json
|
|
111
|
+
cpSync('migrations', 'dist/migrations', { recursive: true }); // when present
|
|
104
112
|
execSync(`zip -r -X "${zipPath}" . -x ".*" -x "__MACOSX*"`, { cwd: 'dist' });
|
|
105
113
|
```
|
|
106
114
|
|
|
107
|
-
|
|
108
|
-
dotfiles keeps `.DS_Store` out of an archive the installer would otherwise skip anyway
|
|
115
|
+
Three details worth keeping if you write your own: `-X` drops extra file attributes, excluding
|
|
116
|
+
dotfiles keeps `.DS_Store` out of an archive the installer would otherwise skip anyway, and a
|
|
117
|
+
top-level `migrations/` is copied in as written — never bundled, because the Rack imports each file
|
|
118
|
+
on its own.
|
|
109
119
|
|
|
110
120
|
Upload the result through the admin API, or from the deployment:
|
|
111
121
|
|
package/dist/docs/auth.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
- [Declaring Attributes](#declaring-attributes)
|
|
5
5
|
- [Reading the Participant](#reading-the-participant)
|
|
6
6
|
- [Degrading Gracefully](#degrading-gracefully)
|
|
7
|
+
- [Roles](#roles)
|
|
7
8
|
- [Rack Side](#rack-side)
|
|
8
9
|
|
|
9
10
|
<a name="introduction"></a>
|
|
@@ -86,6 +87,59 @@ Design the withheld path first and treat personalisation as an enhancement. An a
|
|
|
86
87
|
`Hello, undefined` when an attribute is missing has failed validation of a kind the Rack cannot
|
|
87
88
|
perform for you.
|
|
88
89
|
|
|
90
|
+
`launch.auth.withheld` lists the requested keys this deployment never releases, which is useful in
|
|
91
|
+
development to tell "not configured here" from "not set for this person". It says nothing about any
|
|
92
|
+
one participant.
|
|
93
|
+
|
|
94
|
+
<a name="roles"></a>
|
|
95
|
+
## Roles
|
|
96
|
+
|
|
97
|
+
Roles answer a different question: *what part does this person play in this applet?* They need no
|
|
98
|
+
capability. Every launch carries the participant's roles, resolved by the Rack:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
Rack.auth.roles(); // ['member', 'applet-author', 'judge']
|
|
102
|
+
Rack.auth.hasRole('judge'); // true
|
|
103
|
+
Rack.auth.isGuest(); // false
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Every Rack resolves four built-in roles for every applet:
|
|
107
|
+
|
|
108
|
+
| Role | Held by |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `member` | A signed-in Rack user. |
|
|
111
|
+
| `guest` | A cookie-keyed [guest](installation.md#participants-and-guests). Exactly one of `member` and `guest` is present. |
|
|
112
|
+
| `applet-author` | The applet's owner. |
|
|
113
|
+
| `rack-administrator` | A user the `ziprack.admin` gate allows. |
|
|
114
|
+
|
|
115
|
+
An applet adds its own in the manifest's [`roles`](manifest.md#roles) block. Each declared role has
|
|
116
|
+
a `grant`, which says who holds it before the Rack assigns anyone:
|
|
117
|
+
|
|
118
|
+
| `grant` | Held by |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `everyone` | Every participant. |
|
|
121
|
+
| `members` | Every signed-in participant. |
|
|
122
|
+
| `guests` | Every guest. |
|
|
123
|
+
| `assigned` | Nobody, until the Rack assigns it. The default. |
|
|
124
|
+
|
|
125
|
+
The Rack then **assigns** declared roles to signed-in participants, per applet: to a Rack user, to
|
|
126
|
+
everyone holding one of the Rack's own roles (an administrator, say), to an email address, or to
|
|
127
|
+
every address at a domain. An assignment adds to the grant; it never takes the role away from
|
|
128
|
+
someone the grant gives it to. Guests receive grants only, never assignments.
|
|
129
|
+
|
|
130
|
+
Roles are resolved at launch, so an assignment applies from the participant's next launch. An
|
|
131
|
+
assignment to a role the current version no longer declares is kept, but ignored until a version
|
|
132
|
+
declares the role again.
|
|
133
|
+
|
|
134
|
+
> **Roles inform your code; nothing enforces them yet.** No capability checks a role server-side:
|
|
135
|
+
> a `db@1` collection's `permissions` and a broadcast's `facilitator_events` know nothing about
|
|
136
|
+
> them. Use roles to decide what to render and which actions to offer. Do not rely on them to keep a
|
|
137
|
+
> determined participant out of data the manifest's scopes and permissions already expose.
|
|
138
|
+
|
|
139
|
+
Declared roles are not the room's `facilitator` and `participant`. Those belong to
|
|
140
|
+
[`broadcast@1`](broadcast.md#the-facilitator-role), change during a session, and are enforced by
|
|
141
|
+
the Rack.
|
|
142
|
+
|
|
89
143
|
<a name="rack-side"></a>
|
|
90
144
|
## Rack Side
|
|
91
145
|
|
|
@@ -104,3 +158,25 @@ Admin endpoints:
|
|
|
104
158
|
To source attributes from SSO claims instead, bind
|
|
105
159
|
`ZipRack\Auth\Contracts\ProfileAttributeSource`; `ClaimMapSource` is the default implementation and
|
|
106
160
|
a reasonable starting point.
|
|
161
|
+
|
|
162
|
+
Role assignments are core, so they work whether or not `ziprack/auth` is installed:
|
|
163
|
+
|
|
164
|
+
| Route | Purpose |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `GET /ziprack/admin/applets/{applet}/roles` | `roles` (the current version's, each with its `assignments`), `undeclared` (assignments to roles it no longer declares), `built_in`, `rack_roles`. |
|
|
167
|
+
| `PUT /ziprack/admin/applets/{applet}/roles/{role}` | Replace one role's assignments: `{ assignments: [{ type, value }] }`. |
|
|
168
|
+
|
|
169
|
+
| `type` | `value` |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `user` | A Rack user's id. |
|
|
172
|
+
| `rack_role` | A key from `RackRoles::all()`. |
|
|
173
|
+
| `email` | An address, compared case-insensitively. |
|
|
174
|
+
| `domain` | `example.com` (a leading `@` is dropped). Matches that domain exactly, not its subdomains. |
|
|
175
|
+
|
|
176
|
+
The Rack's own roles come from `ZipRack\Core\Contracts\RackRoles`: `all()` returns every key and
|
|
177
|
+
label, `of($user)` the keys one user holds. The default, `GateRackRoles`, knows one, `administrator`,
|
|
178
|
+
from the `ziprack.admin` gate. A Rack with its own roles binds its implementation:
|
|
179
|
+
|
|
180
|
+
```php
|
|
181
|
+
$this->app->singleton(RackRoles::class, MyRackRoles::class);
|
|
182
|
+
```
|
package/dist/docs/broadcast.md
CHANGED
|
@@ -26,7 +26,7 @@ Transport is Laravel Reverb. That is a deployment concern: nothing in the applet
|
|
|
26
26
|
<a name="rooms"></a>
|
|
27
27
|
## Rooms
|
|
28
28
|
|
|
29
|
-
A **room** is an applet
|
|
29
|
+
A **room** is an applet plus its members. Every member has exactly one of two roles —
|
|
30
30
|
`participant` or `facilitator` — and membership is orthogonal to attempts: a room may contain
|
|
31
31
|
attempts (each person running their own inside a shared session) or none at all.
|
|
32
32
|
|
package/dist/docs/conformance.md
CHANGED
|
@@ -28,19 +28,18 @@ The suite runs inside a transaction that is rolled back, so it is safe against a
|
|
|
28
28
|
check builds its own fixture — a temporary applet, a version, a package — and cleans up after itself.
|
|
29
29
|
|
|
30
30
|
```
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
✓ attempts are pinned
|
|
40
|
-
✓ statements are append-only
|
|
41
|
-
✓ records are scoped and validated
|
|
31
|
+
PASS A manifest missing required fields fails validation with the expected error
|
|
32
|
+
PASS The installer rejects zips containing traversal, absolute paths, symlinks or disallowed files
|
|
33
|
+
PASS An applet requesting a capability this Rack does not implement is refused at install
|
|
34
|
+
…
|
|
35
|
+
PASS db@1: records are schema-validated, quota-limited and invisible across scopes
|
|
36
|
+
PASS db@1: applet data carries over to a new version, and a version that would lose some is not promoted silently
|
|
37
|
+
|
|
38
|
+
18 checks, 0 failed.
|
|
42
39
|
```
|
|
43
40
|
|
|
41
|
+
A failing check prints `FAIL`, its name and the reason, and the command exits non-zero.
|
|
42
|
+
|
|
44
43
|
<a name="what-it-checks"></a>
|
|
45
44
|
## What It Checks
|
|
46
45
|
|
|
@@ -55,17 +54,19 @@ Core checks run on every Rack:
|
|
|
55
54
|
| `CspIsTightened` | The response CSP contains the required directives, and `img-src` does not contain `https:`. |
|
|
56
55
|
| `RetiredVersionIsGone` | A retired version can no longer be launched. |
|
|
57
56
|
| `PromotionRequiresReview` | A version adding a capability cannot be promoted unapproved. |
|
|
57
|
+
| `RolesAreDeclaredAndResolved` | A manifest declaring a built-in role fails validation, and a guest launch resolves exactly `guest` plus the roles granted to everyone and to guests. |
|
|
58
58
|
|
|
59
59
|
Each installed capability contributes its own, so the suite grows with the Rack:
|
|
60
60
|
|
|
61
61
|
| Check | Capability |
|
|
62
62
|
|---|---|
|
|
63
|
-
| `CompletedAttemptIsFrozen`, `AttemptsArePinned` | `attempt` |
|
|
63
|
+
| `CompletedAttemptIsFrozen`, `AttemptsArePinned`, `PreviewAttemptsAreSeparate` | `attempt` |
|
|
64
64
|
| `StatementsAreAppendOnly` | `activity` |
|
|
65
65
|
| `RecordsAreScopedAndValidated` | `db` |
|
|
66
|
+
| `DataSurvivesPromotion` | `db` — applet records carry over to a new current version, and a version whose schema rejects them is refused with `DataCutoverRequired` rather than promoted |
|
|
66
67
|
| `UploadsAreScopedAndTyped` | `storage` |
|
|
67
68
|
| `AiBudgetsAreEnforced` | `ai` |
|
|
68
|
-
| `FacilitatorEventsAreEnforced` | `broadcast` |
|
|
69
|
+
| `FacilitatorEventsAreEnforced`, `InviteNeedsAnOpenRoom` | `broadcast` |
|
|
69
70
|
|
|
70
71
|
<a name="in-ci"></a>
|
|
71
72
|
## In CI
|