@neta-art/cohub 8.17.0 → 8.19.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/README.md +17 -9
- package/dist/chunks/environment.d.ts +525 -5
- package/dist/chunks/environment.js +1 -0
- package/dist/chunks/http.d.ts +51 -2
- package/dist/chunks/http.js +975 -943
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/http.d.ts +2 -2
- package/dist/index.d.ts +35 -4
- package/dist/index.js +834 -30
- package/dist/protocol/dist/app-authorization.d.ts +1 -0
- package/dist/protocol/dist/index.d.ts +4 -0
- package/dist/protocol/dist/model/session.d.ts +1 -0
- package/dist/protocol/dist/model/turn.d.ts +1 -0
- package/dist/protocol/dist/realtime/types.d.ts +2 -0
- package/dist/protocol/dist/runtime/archive.d.ts +1 -0
- package/dist/protocol/dist/runtime/context.d.ts +1 -0
- package/dist/protocol/dist/runtime/index.d.ts +3 -0
- package/dist/types.d.ts +1 -0
- package/docs/app-runtime-guide.md +145 -92
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "./app-authorization.js";
|
|
1
2
|
import { BOARD_STROKE_MAX_SIZE, BOARD_STROKE_MIN_SIZE, BoardCapability, BoardCoordinateSpace, BoardRenderCost, clampBoardStrokeSize } from "./board-constants.js";
|
|
2
3
|
import { AUTO_BOARD_CONNECTION_ANCHOR, BOARD_CONNECTION_DIRECTIONS, BOARD_CONNECTION_LINES, BOARD_CONNECTION_ROUTINGS, BOARD_CONNECTION_SIDES, BoardConnection, BoardConnectionAnchor, BoardConnectionAnchorSchema, BoardConnectionDirection, BoardConnectionEndpoint, BoardConnectionEndpointSchema, BoardConnectionInput, BoardConnectionLine, BoardConnectionPatch, BoardConnectionPatchSchema, BoardConnectionRecord, BoardConnectionRouting, BoardConnectionRoutingConfig, BoardConnectionRoutingSchema, BoardConnectionSchema, BoardConnectionSide, BoardConnectionStyle, BoardConnectionStyleSchema, BoardConnectionWaypointSchema, BoardRelationSchema, DEFAULT_BOARD_CONNECTION_ROUTING, DEFAULT_BOARD_CONNECTION_STYLE, DEFAULT_BOARD_RELATION, connectionItemIds, connectionOtherItemId, connectionTouchesItem, createBoardConnection, flipBoardConnection, normalizeBoardConnectionStyle } from "./board-connection.js";
|
|
3
4
|
import { BoardAnimationTarget, BoardAnimationTargetSchema, BoardComposition, BoardCompositionInput, BoardCompositionInputSchema, BoardCompositionSchema, BoardEasing, BoardEasingSchema, BoardProceduralClip, BoardProceduralClipSchema, BoardTimelineMarker, BoardTimelineMarkerSchema, BoardTrack, BoardTrackInterpolation, BoardTrackSchema } from "./board-composition.js";
|
|
@@ -11,6 +12,9 @@ import { BOARD_COLOR_IDS, BOARD_GEO_KINDS, BoardColorId, BoardGeoKind } from "./
|
|
|
11
12
|
import "./board-codec.js";
|
|
12
13
|
import "./board-content.js";
|
|
13
14
|
import { BoardArrowGeometryInput, boardArrowFrame, boardDrawBounds, boardDrawSampleRadius } from "./board-geometry.js";
|
|
15
|
+
import "./runtime/index.js";
|
|
16
|
+
import "./model/turn.js";
|
|
17
|
+
import "./model/session.js";
|
|
14
18
|
import "./realtime/board-awareness.js";
|
|
15
19
|
import "./app.js";
|
|
16
20
|
import "./realtime/types.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./turn.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../runtime/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./index.js";
|
package/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { BoardAnimationTarget, BoardComposition, BoardEasing, BoardProceduralCli
|
|
|
3
3
|
import { BoardAssetRef, BoardEffect, BoardEffectInput } from "./protocol/dist/board-effect.js";
|
|
4
4
|
import { BoardAuthoringItem, BoardAuthoringSnapshot, BoardSemanticCommand } from "./protocol/dist/board-authoring.js";
|
|
5
5
|
import { BoardCameraFocus, BoardCameraFocusParams, BoardCameraState, BoardDiagnostic, BoardManifest, BoardMutationReceipt, BoardPlaybackSnapshot, BoardRecord, BoardTransactionOperation, BoardTransactionRecord, BoardTransactionsPage, BoardValidationResult } from "./protocol/dist/board.js";
|
|
6
|
+
import "./protocol/dist/model/session.js";
|
|
6
7
|
import "./protocol/dist/index.js";
|
|
7
8
|
//#region src/types.d.ts
|
|
8
9
|
type UserProfile = {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Cohub App Runtime Guide
|
|
2
2
|
|
|
3
|
-
This guide explains
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This guide explains the Cohub SDK in a published App iframe or an explicitly
|
|
4
|
+
configured standalone broker. New Apps should use `auth.authorize()`; see the
|
|
5
|
+
[authorization contract](../../../docs/app-authorization.md). Legacy APIs below
|
|
6
|
+
retain their original return types. Read this before building any
|
|
6
7
|
App that calls Cohub capabilities from browser-side JavaScript.
|
|
7
8
|
|
|
8
9
|
It is written to be self-contained: an agent or developer who reads only this
|
|
@@ -56,7 +57,7 @@ requiring the viewer to paste an API key.
|
|
|
56
57
|
│ │ │ │
|
|
57
58
|
│ │ createCohubClient() ──► token ──►│──┼──► Cohub API
|
|
58
59
|
│ │ client.context() ◄── identity │ │
|
|
59
|
-
│ │ client.auth.
|
|
60
|
+
│ │ client.auth.authorize() ► consent│ │
|
|
60
61
|
│ └───────────────────────────────────┘ │
|
|
61
62
|
└─────────────────────────────────────────┘
|
|
62
63
|
```
|
|
@@ -65,17 +66,15 @@ These runtime-only APIs form the foundation; everything else is standard SDK:
|
|
|
65
66
|
|
|
66
67
|
| API | What it does | Returns |
|
|
67
68
|
|---|---|---|
|
|
68
|
-
| `client.context()` | Asks the host for the App's identity | `{ app, space, viewer?, invocation?, shell?, permissions }` or `null` |
|
|
69
|
-
| `client.auth.
|
|
70
|
-
| `client.auth.
|
|
71
|
-
| `client.auth.requestCreateSpace({ scopes, space, reason })` | One consent: create a viewer-owned Space (`CreateSpaceInput`) and grant the scopes on it | `{ granted, space }` |
|
|
69
|
+
| `client.context()` | Asks the host for the App's identity | `{ app, space, viewer?, invocation?, shell?, permissions, capabilities?, mode? }` or `null` |
|
|
70
|
+
| `client.auth.authorize({ target, scopes, reason?, alwaysAsk?, fallback? })` | Requests consent for an account, a Space, or a viewer-picked Space; silent when a grant already covers the scopes | `{ status: "granted", requestedTarget, target, resolution, grant }` \| `{ status: "cancelled" }` \| `{ status: "denied", code }` |
|
|
71
|
+
| `client.auth.request()` / `requestSpace()` / `requestCreateSpace()` | Legacy entry points, kept for older Apps | original shapes (`boolean`, `{ granted, space }`) |
|
|
72
72
|
| `client.context().permissions.viewerGrants` | Render the viewer's current per-space grants | `{ spaceId, scopes }[]` |
|
|
73
73
|
| `client.app.commerce.*` / `client.app.realtime.*` | Commerce and realtime, bound to the app's runtime identity | (see below) |
|
|
74
74
|
|
|
75
|
-
> **Runtime
|
|
76
|
-
>
|
|
77
|
-
>
|
|
78
|
-
> fail. Always develop against a published App.
|
|
75
|
+
> **Runtime requirement.** Use a published App iframe or explicitly configure
|
|
76
|
+
> a broker. A standalone page without broker configuration has no runtime.
|
|
77
|
+
> Initialize `context()` before using APIs to discover Host capabilities.
|
|
79
78
|
|
|
80
79
|
`client.app.onContextChanged(listener)` pushes a fresh context whenever the
|
|
81
80
|
host's state changes (sign-in, invocation, grants), so an App can render its
|
|
@@ -100,7 +99,7 @@ and context. This is the normal case when a viewer opens an App through Cohub.
|
|
|
100
99
|
snapshot with the originating `spaceId`, `sessionId`, `turnId`, and
|
|
101
100
|
`toolCallId` when available. The invocation Space may differ from the App's
|
|
102
101
|
own `space.id`.
|
|
103
|
-
- `client.auth.
|
|
102
|
+
- `client.auth.authorize()` triggers an in-shell consent flow (no popup window).
|
|
104
103
|
|
|
105
104
|
```js
|
|
106
105
|
const ctx = await client.context();
|
|
@@ -118,9 +117,11 @@ The App is accessed as a standalone page (`window.parent === window`), e.g.
|
|
|
118
117
|
a direct static-asset URL not wrapped in the Cohub iframe. The SDK opens a
|
|
119
118
|
**popup window** to a Cohub auth-broker page to obtain tokens.
|
|
120
119
|
|
|
121
|
-
- `client.context()` is **answered locally** by the SDK:
|
|
122
|
-
**empty string `""`**, and viewer grants
|
|
123
|
-
|
|
120
|
+
- `client.context()` is **answered locally** by the SDK: it reports
|
|
121
|
+
`mode: "broker"`, `space.id` is an **empty string `""`**, and viewer grants
|
|
122
|
+
are unavailable (empty). Broker tokens stay in memory and are never restored
|
|
123
|
+
from or written to `localStorage`.
|
|
124
|
+
- `client.auth.authorize()` opens a popup to
|
|
124
125
|
`${brokerOrigin}/app-auth?app=${appId}`.
|
|
125
126
|
|
|
126
127
|
> **Broker mode requires configuration.** You must pass `app: { brokerOrigin,
|
|
@@ -134,42 +135,43 @@ a direct static-asset URL not wrapped in the Cohub iframe. The SDK opens a
|
|
|
134
135
|
|
|
135
136
|
```js
|
|
136
137
|
const ctx = await client.context();
|
|
137
|
-
const isBroker =
|
|
138
|
+
const isBroker = ctx?.mode === "broker";
|
|
138
139
|
```
|
|
139
140
|
|
|
140
141
|
In broker mode you cannot get a spaceId from `context()`. Resolve it via the
|
|
141
142
|
public by-slug App API (anonymous, no token needed):
|
|
142
143
|
|
|
143
144
|
```js
|
|
144
|
-
const isBroker =
|
|
145
|
+
const isBroker = ctx?.mode === "broker";
|
|
145
146
|
let spaceId;
|
|
146
147
|
if (isBroker) {
|
|
147
148
|
const detail = await client.apps.getBySlug(ownerUsername, spaceSlug, appSlug);
|
|
148
149
|
spaceId = detail.app.spaceId;
|
|
149
150
|
} else {
|
|
150
|
-
|
|
151
|
+
// Prefer where the App is running; fall back to the Space that owns it.
|
|
152
|
+
spaceId = ctx.shell?.space?.id ?? ctx.invocation?.spaceId ?? ctx.app.homeSpace?.id;
|
|
151
153
|
}
|
|
152
154
|
```
|
|
153
155
|
|
|
154
156
|
### Broker mode: user-activation ordering gotcha
|
|
155
157
|
|
|
156
158
|
`HttpTransport` calls `getAccessToken()` on **every** request — including
|
|
157
|
-
public ones like `apps.getBySlug()`. In broker mode,
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
public ones like `apps.getBySlug()`. In broker mode, a token exchange opens a
|
|
160
|
+
popup, which **consumes the browser's user-activation budget**. If a second
|
|
161
|
+
popup (the consent dialog) follows in the same click, the browser blocks it.
|
|
160
162
|
|
|
161
|
-
**Fix:** call `auth.
|
|
162
|
-
`getAccessToken()`.
|
|
163
|
-
|
|
164
|
-
popup.
|
|
163
|
+
**Fix:** call `auth.authorize()` **before** any other API call that triggers
|
|
164
|
+
`getAccessToken()`. The token returned by that exchange is cached in memory for
|
|
165
|
+
the lifetime of the client, so later `getAccessToken()` calls reuse it without
|
|
166
|
+
a popup.
|
|
165
167
|
|
|
166
168
|
```js
|
|
167
|
-
// WRONG: getBySlug() opens a popup, consumes activation,
|
|
169
|
+
// WRONG: getBySlug() opens a popup, consumes activation, consent popup blocked
|
|
168
170
|
const detail = await client.apps.getBySlug(owner, spaceSlug, appSlug);
|
|
169
|
-
await client.auth.
|
|
171
|
+
await client.auth.authorize({ target: { kind: "pick-space" }, scopes });
|
|
170
172
|
|
|
171
|
-
// RIGHT:
|
|
172
|
-
await client.auth.
|
|
173
|
+
// RIGHT: authorize() opens the only popup, then getBySlug() hits the token cache
|
|
174
|
+
await client.auth.authorize({ target: { kind: "pick-space" }, scopes });
|
|
173
175
|
const detail = await client.apps.getBySlug(owner, spaceSlug, appSlug);
|
|
174
176
|
```
|
|
175
177
|
|
|
@@ -207,7 +209,7 @@ command.execute — run sandbox shell commands
|
|
|
207
209
|
### Viewer grants (consent-required, any permission, per Space)
|
|
208
210
|
|
|
209
211
|
A viewer grants these through a consent dialog triggered by
|
|
210
|
-
`client.auth.
|
|
212
|
+
`client.auth.authorize()`. A viewer may grant
|
|
211
213
|
**any** permission they currently hold on the target Space — including scopes
|
|
212
214
|
outside the eight app scopes, such as `generation.create` or the account-level
|
|
213
215
|
`user.*` scopes. Two hard rules are enforced by the server:
|
|
@@ -240,51 +242,77 @@ requested Space.
|
|
|
240
242
|
|
|
241
243
|
### Requesting viewer grants
|
|
242
244
|
|
|
243
|
-
|
|
245
|
+
Consent must be requested **from a user gesture** (a button click). It is
|
|
244
246
|
silent when an existing grant already covers the scopes — the dialog only
|
|
245
247
|
opens when something new is needed:
|
|
246
248
|
|
|
247
249
|
```js
|
|
248
|
-
// Target a known Space
|
|
249
|
-
|
|
250
|
+
// 1. Target a known Space. The result names the Space that was actually
|
|
251
|
+
// granted, which may differ from the request when the viewer cannot use it.
|
|
252
|
+
const result = await client.auth.authorize({
|
|
253
|
+
target: { kind: "space", spaceId: invocationSpaceId },
|
|
250
254
|
scopes: ["taskrun.view"],
|
|
251
|
-
spaceId: invocationSpaceId,
|
|
252
255
|
reason: "This app reads generation tasks in the Space you opened it from.",
|
|
253
256
|
});
|
|
257
|
+
if (result.status === "granted" && result.target.kind === "space") {
|
|
258
|
+
const space = client.space(result.target.spaceId);
|
|
259
|
+
}
|
|
254
260
|
|
|
255
|
-
// One consent: the viewer picks the Space. The host loads the space list —
|
|
256
|
-
//
|
|
257
|
-
const
|
|
261
|
+
// 2. One consent: the viewer picks the Space. The host loads the space list —
|
|
262
|
+
// the app only learns the Space the viewer chose.
|
|
263
|
+
const picked = await client.auth.authorize({
|
|
264
|
+
target: { kind: "pick-space" },
|
|
258
265
|
scopes: ["file.view", "session.view"],
|
|
259
266
|
reason: "This app reads the Space you pick.",
|
|
260
267
|
});
|
|
261
|
-
if (granted && space) {
|
|
262
|
-
const
|
|
268
|
+
if (picked.status === "granted" && picked.target.kind === "space") {
|
|
269
|
+
const space = client.space(picked.target.spaceId);
|
|
263
270
|
}
|
|
264
271
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
scopes: ["
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
// 3. Account-level data needs an account target, not a Space.
|
|
273
|
+
const account = await client.auth.authorize({
|
|
274
|
+
target: { kind: "account" },
|
|
275
|
+
scopes: ["user.space.list"],
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// 4. Strict target: never resolve to another Space.
|
|
279
|
+
const strict = await client.auth.authorize({
|
|
280
|
+
target: { kind: "space", spaceId: projectSpaceId },
|
|
281
|
+
scopes: ["file.edit"],
|
|
282
|
+
fallback: "none",
|
|
274
283
|
});
|
|
275
|
-
if (created.granted && created.space) {
|
|
276
|
-
const next = client.space(created.space.id);
|
|
277
|
-
}
|
|
278
|
-
// `granted: false` with a `space` means the Space was created but bootstrap
|
|
279
|
-
// did not finish — no grant was issued. A deny is `{ granted: false, space: null }`.
|
|
280
284
|
```
|
|
281
285
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
`
|
|
286
|
+
The result tells the App exactly what happened:
|
|
287
|
+
|
|
288
|
+
| Field | Meaning |
|
|
289
|
+
|---|---|
|
|
290
|
+
| `status` | `granted`, `cancelled`, or `denied` |
|
|
291
|
+
| `requestedTarget` | What the App asked for |
|
|
292
|
+
| `target` | What was actually granted (`account`, or a Space with `spaceId`/`name`) |
|
|
293
|
+
| `resolution` | `requested`, `selected`, or `fallback` |
|
|
294
|
+
| `grant` | `{ id, spaceId, scopes, expiresAt }` from the server |
|
|
295
|
+
|
|
296
|
+
Rules worth internalizing:
|
|
297
|
+
|
|
298
|
+
- **Act on `target`, never on the request.** API calls are never redirected to
|
|
299
|
+
another Space on your behalf; if you asked for Space A and got B, use B.
|
|
300
|
+
- **Unavailable Spaces fall back by default.** The dialog shows the
|
|
301
|
+
viewer-controlled Space that will be used. Pass `fallback: "none"` when the
|
|
302
|
+
operation only makes sense in the Space you named.
|
|
303
|
+
- **Cancellation is not an error.** `{ status: "cancelled" }` means the viewer
|
|
304
|
+
closed the dialog; keep their input and offer the action again.
|
|
305
|
+
- **Incremental growth.** New consent keeps scopes from a still-valid grant and
|
|
306
|
+
adds the new ones; it never revives an expired or revoked scope.
|
|
307
|
+
- Pass `alwaysAsk: true` to skip silent reuse — for re-confirming a grant or
|
|
308
|
+
letting the viewer switch to another Space.
|
|
309
|
+
|
|
310
|
+
Creating a viewer-owned Space stays on `client.auth.requestCreateSpace({ space, scopes })`
|
|
311
|
+
(never silent, each confirm mints a new Space). `{ granted: false }` **may still
|
|
312
|
+
carry a `space`**: the Space was created but bootstrap or authorization did not
|
|
313
|
+
finish. Keep the id, resume against it, and never create a second Space or
|
|
314
|
+
delete it automatically. An App can also create a viewer-owned Space directly
|
|
315
|
+
with `client.spaces.create()` once it holds a `space.create` viewer grant.
|
|
288
316
|
|
|
289
317
|
### Checking grant state at runtime
|
|
290
318
|
|
|
@@ -300,7 +328,7 @@ ctx.permissions.viewerScopes // flat viewer scopes (legacy compatibility)
|
|
|
300
328
|
```
|
|
301
329
|
|
|
302
330
|
Apps never cache or manage grants themselves — the host does. Checking state
|
|
303
|
-
is for **rendering**; acting is `auth.
|
|
331
|
+
is for **rendering**; acting is `auth.authorize()`'s job.
|
|
304
332
|
|
|
305
333
|
### Managing grants
|
|
306
334
|
|
|
@@ -340,6 +368,7 @@ dialog can.
|
|
|
340
368
|
| List tasks in a Space | `client.tasks.list({ spaceId })` | `taskrun.view` on that Space | app or viewer |
|
|
341
369
|
| List all owned task runs | `client.tasks.list()` | `user.taskrun.list` | **viewer only** |
|
|
342
370
|
| Create a Space for the viewer | `client.auth.requestCreateSpace({ space, scopes })` | requested scopes on the new Space | **viewer consent** |
|
|
371
|
+
| Request any consent | `client.auth.authorize({ target, scopes })` | the requested scopes on the resolved target | **viewer consent** |
|
|
343
372
|
| Create a viewer-owned Space directly | `client.spaces.create(input)` | `space.create` | **viewer only** |
|
|
344
373
|
| List viewer's spaces | `client.spaces.list()` | `user.space.list` | **viewer only** |
|
|
345
374
|
| List viewer's sessions | `client.user.listSessions()` | `user.session.list` | **viewer only** |
|
|
@@ -367,7 +396,7 @@ App's own Space; any other Space needs a viewer grant on that Space.
|
|
|
367
396
|
|
|
368
397
|
**Cross-space app** (acts on Spaces the viewer picks):
|
|
369
398
|
- `appScopes: []` (or its own-Space needs)
|
|
370
|
-
- viewer grants via `auth.
|
|
399
|
+
- viewer grants via `auth.authorize({ target: { kind: "pick-space" } })` at runtime
|
|
371
400
|
|
|
372
401
|
---
|
|
373
402
|
|
|
@@ -458,28 +487,36 @@ the Cohub iframe both are ignored (bridge mode).
|
|
|
458
487
|
// 1. Create client (env is mandatory in the browser)
|
|
459
488
|
const client = createCohubClient({ env: isDevApp ? "dev" : "prod" });
|
|
460
489
|
|
|
461
|
-
// 2. Get runtime context (and keep it fresh)
|
|
490
|
+
// 2. Get runtime context (and keep it fresh). `app` is present in bridge and
|
|
491
|
+
// broker mode; a Space id is only reported by the bridge.
|
|
462
492
|
const ctx = await client.context();
|
|
463
|
-
if (!ctx?.
|
|
464
|
-
// Not in an app runtime (or broker mode — see §2)
|
|
493
|
+
if (!ctx?.app?.id) {
|
|
465
494
|
throw new Error("Not running inside a published app.");
|
|
466
495
|
}
|
|
467
496
|
const stopContextWatch = client.app.onContextChanged((next) => renderGrants(next));
|
|
468
497
|
|
|
469
|
-
// 3.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
498
|
+
// 3. Request viewer grants from a user gesture (e.g. button click). Target
|
|
499
|
+
// where the App is running, or `{ kind: "pick-space" }` when it has no
|
|
500
|
+
// location of its own.
|
|
501
|
+
const consent = await client.auth.authorize({
|
|
502
|
+
target: {
|
|
503
|
+
kind: "space",
|
|
504
|
+
spaceId: ctx.shell?.space?.id ?? ctx.invocation?.spaceId ?? ctx.app.homeSpace?.id,
|
|
505
|
+
},
|
|
474
506
|
scopes: ["session.prompt.fullaccess", "generation.create"],
|
|
475
507
|
reason: "This app sends prompts and generates images.",
|
|
476
508
|
});
|
|
509
|
+
if (consent.status !== "granted" || consent.target.kind !== "space") {
|
|
510
|
+
throw new Error("Consent was not granted.");
|
|
511
|
+
}
|
|
477
512
|
|
|
478
|
-
//
|
|
479
|
-
|
|
513
|
+
// 4. Call capabilities on the Space that was actually granted — never on the
|
|
514
|
+
// one you asked for.
|
|
515
|
+
const activeSpace = client.space(consent.target.spaceId);
|
|
516
|
+
const result = await activeSpace.prompt({ content: [{ type: "text", text: "Hello" }] });
|
|
480
517
|
```
|
|
481
518
|
|
|
482
|
-
> **`auth.
|
|
519
|
+
> **`auth.authorize()` must be called from a user gesture** (click handler).
|
|
483
520
|
> Browsers block popups (broker mode) and some consent flows (bridge mode)
|
|
484
521
|
> when triggered programmatically without user activation. Do not call it on
|
|
485
522
|
> page load. It is safe to call repeatedly: covered scopes renew silently.
|
|
@@ -581,7 +618,8 @@ send must match — the backend picks the permission check based on
|
|
|
581
618
|
|
|
582
619
|
```js
|
|
583
620
|
// 1. Ensure the read-only scope is granted (silent when already covered)
|
|
584
|
-
await client.auth.
|
|
621
|
+
await client.auth.authorize({
|
|
622
|
+
target: { kind: "space", spaceId: space.id },
|
|
585
623
|
scopes: ["session.prompt.readonly"],
|
|
586
624
|
reason: "Generate a one-off character reply (read-only).",
|
|
587
625
|
});
|
|
@@ -708,21 +746,24 @@ not the App's own space. Each requires a separate viewer grant.
|
|
|
708
746
|
|
|
709
747
|
```js
|
|
710
748
|
// List the viewer's spaces — needs user.space.list
|
|
711
|
-
await client.auth.
|
|
749
|
+
await client.auth.authorize({
|
|
750
|
+
target: { kind: "account" },
|
|
712
751
|
scopes: ["user.space.list"],
|
|
713
752
|
reason: "Show your space list.",
|
|
714
753
|
});
|
|
715
754
|
const spaces = await client.spaces.list();
|
|
716
755
|
|
|
717
756
|
// List sessions across all the viewer's spaces — needs user.session.list
|
|
718
|
-
await client.auth.
|
|
757
|
+
await client.auth.authorize({
|
|
758
|
+
target: { kind: "account" },
|
|
719
759
|
scopes: ["user.session.list"],
|
|
720
760
|
reason: "List your recent sessions.",
|
|
721
761
|
});
|
|
722
762
|
const { sessions } = await client.user.listSessions({ limit: 20 });
|
|
723
763
|
|
|
724
764
|
// Read activity — needs user.usage.read
|
|
725
|
-
await client.auth.
|
|
765
|
+
await client.auth.authorize({
|
|
766
|
+
target: { kind: "account" },
|
|
726
767
|
scopes: ["user.usage.read"],
|
|
727
768
|
reason: "Show your activity.",
|
|
728
769
|
});
|
|
@@ -1007,19 +1048,21 @@ function log(el, msg) {
|
|
|
1007
1048
|
// --- Runtime initialization ---
|
|
1008
1049
|
async function ensureRuntime(outEl) {
|
|
1009
1050
|
const ctx = await client.context();
|
|
1010
|
-
if (!ctx?.
|
|
1051
|
+
if (!ctx?.app?.id) {
|
|
1011
1052
|
throw new Error("Not running inside a published app runtime.");
|
|
1012
1053
|
}
|
|
1013
|
-
|
|
1054
|
+
// Prefer the current Cohub location, then the Space that owns the App.
|
|
1055
|
+
spaceId = ctx.shell?.space?.id ?? ctx.invocation?.spaceId ?? ctx.app.homeSpace?.id;
|
|
1056
|
+
if (!spaceId) throw new Error("No Space to act on — request one with auth.authorize().");
|
|
1014
1057
|
space = client.space(spaceId);
|
|
1015
1058
|
return ctx;
|
|
1016
1059
|
}
|
|
1017
1060
|
|
|
1018
1061
|
// --- Viewer grant helpers ---
|
|
1019
|
-
// Render state from context; act through auth.
|
|
1062
|
+
// Render state from context; act through auth.authorize (silent when covered).
|
|
1020
1063
|
function hasViewerGrant(ctx, scope, spaceId) {
|
|
1021
1064
|
return (ctx?.permissions?.viewerGrants ?? []).some(
|
|
1022
|
-
(g) => g.spaceId === (spaceId ?? ctx?.
|
|
1065
|
+
(g) => g.spaceId === (spaceId ?? ctx?.app?.homeSpace?.id) && g.scopes.includes(scope),
|
|
1023
1066
|
);
|
|
1024
1067
|
}
|
|
1025
1068
|
|
|
@@ -1032,9 +1075,17 @@ async function ensureViewerScopes(scopes, reason, outEl) {
|
|
|
1032
1075
|
return true;
|
|
1033
1076
|
}
|
|
1034
1077
|
log(outEl, `Requesting: [${missing.join(", ")}]...`);
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1078
|
+
const result = await client.auth.authorize({
|
|
1079
|
+
target: { kind: "space", spaceId: ctx?.shell?.space?.id ?? ctx?.invocation?.spaceId },
|
|
1080
|
+
scopes,
|
|
1081
|
+
reason,
|
|
1082
|
+
});
|
|
1083
|
+
if (result.status === "granted") {
|
|
1084
|
+
log(outEl, `Authorized on ${result.target.kind === "space" ? result.target.spaceId : "account"}.`);
|
|
1085
|
+
} else {
|
|
1086
|
+
log(outEl, result.status === "cancelled" ? "Consent cancelled." : `Consent denied (${result.code}).`);
|
|
1087
|
+
}
|
|
1088
|
+
return result.status === "granted";
|
|
1038
1089
|
}
|
|
1039
1090
|
|
|
1040
1091
|
// --- LLM chat (space.prompt + subscribeGeneration) ---
|
|
@@ -1121,7 +1172,7 @@ async function generateImage(prompt, outEl) {
|
|
|
1121
1172
|
return image?.source?.url ?? null;
|
|
1122
1173
|
}
|
|
1123
1174
|
|
|
1124
|
-
// --- Event handlers (auth.
|
|
1175
|
+
// --- Event handlers (auth.authorize must be in a user gesture) ---
|
|
1125
1176
|
$("btn-context").addEventListener("click", async () => {
|
|
1126
1177
|
const out = $("output-context");
|
|
1127
1178
|
out.textContent = "";
|
|
@@ -1192,7 +1243,8 @@ $("btn-img").addEventListener("click", async () => {
|
|
|
1192
1243
|
if (ctx) {
|
|
1193
1244
|
setOutput($("output-context"), {
|
|
1194
1245
|
"app.id": ctx.app?.id,
|
|
1195
|
-
"
|
|
1246
|
+
"app.homeSpace.id": ctx.app?.homeSpace?.id,
|
|
1247
|
+
mode: ctx.mode,
|
|
1196
1248
|
permissions: ctx.permissions,
|
|
1197
1249
|
});
|
|
1198
1250
|
}
|
|
@@ -1223,18 +1275,19 @@ Before publishing your App, verify each item:
|
|
|
1223
1275
|
`session.view`.
|
|
1224
1276
|
- [ ] **`generation.create` does NOT include `taskrun.view`** — creating a
|
|
1225
1277
|
generation task succeeds but polling the result 403s without `taskrun.view`.
|
|
1226
|
-
- [ ] **`auth.
|
|
1278
|
+
- [ ] **`auth.authorize()` is called from a user gesture** (button click), not
|
|
1227
1279
|
on page load. It is safe to call repeatedly — covered scopes renew silently.
|
|
1228
1280
|
- [ ] **Cross-Space access targets the right Space** — viewer grants are per
|
|
1229
|
-
Space. Pass `
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
`
|
|
1281
|
+
Space. Pass `target: { kind: "space", spaceId }` to name one, or
|
|
1282
|
+
`target: { kind: "pick-space" }` to let the viewer choose. Act on the
|
|
1283
|
+
returned `target`, which may differ from the request. Use
|
|
1284
|
+
`auth.requestCreateSpace` to mint a new viewer-owned Space in one consent, or
|
|
1285
|
+
`spaces.create()` directly once the app holds a `space.create` viewer grant.
|
|
1233
1286
|
- [ ] **`subscribeGeneration` errors are not silently swallowed** — if the
|
|
1234
1287
|
stream fails, surface it; a silent fallback to polling will also 403 if
|
|
1235
1288
|
`session.view` is missing.
|
|
1236
1289
|
- [ ] **Broker mode**: if the App may be accessed standalone, pass
|
|
1237
|
-
`app: { brokerOrigin, appId }` and call `auth.
|
|
1290
|
+
`app: { brokerOrigin, appId }` and call `auth.authorize()` before any other
|
|
1238
1291
|
API call (to avoid user-activation exhaustion).
|
|
1239
1292
|
- [ ] **Space has a slug and owner has a username** before publishing — the
|
|
1240
1293
|
API rejects Apps when either is missing.
|