@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.
@@ -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";
@@ -1,5 +1,7 @@
1
1
  import "../board-composition.js";
2
2
  import "../board-effect.js";
3
3
  import "../board.js";
4
+ import "../model/turn.js";
5
+ import "../model/session.js";
4
6
  import "./board-awareness.js";
5
7
  import "../app.js";
@@ -0,0 +1 @@
1
+ import { z } from "zod";
@@ -0,0 +1 @@
1
+ import "./index.js";
@@ -0,0 +1,3 @@
1
+ import "./archive.js";
2
+ import "./context.js";
3
+ import { z } from "zod";
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 how to use the Cohub SDK **inside a published App** — the
4
- only environment where runtime APIs (`context()`, `auth.request`,
5
- `app.commerce.*`, `app.realtime.*`) function. Read this before building any
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.request() ──► consent│ │
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.request({ scopes, reason, spaceId?, alwaysAsk? })` | Ensures the app holds these scopes; silent when a grant already covers them, consent dialog otherwise | `true` / `false` |
70
- | `client.auth.requestSpace({ scopes, reason, alwaysAsk? })` | One consent: the viewer picks a Space and grants the scopes on it | `{ granted, space }` |
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-only constraint.** These APIs only work inside a **published**
76
- > App. Outside that context (a static asset URL, a local `file://` preview,
77
- > a plain Node script) `context()` returns `null` and the other runtime APIs
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.request()` triggers an in-shell consent flow (no popup window).
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: `space.id` is an
122
- **empty string `""`**, and viewer grants are unavailable (empty).
123
- - `client.auth.request()` opens a popup to
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 = !ctx?.space?.id; // bridge has a real id; broker is ""
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 = !ctx?.space?.id;
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
- spaceId = ctx.space.id;
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, an uncached token request
158
- opens a popup, which **consumes the browser's user-activation budget**. If a
159
- second popup (`auth.request`) follows in the same click, the browser blocks it.
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.request()` **before** any other API call that triggers
162
- `getAccessToken()`. After `auth.request` succeeds, the token is cached in
163
- `localStorage` and subsequent `getAccessToken()` calls hit the cache — no
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, auth.request popup blocked
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.request({ scopes, reason });
171
+ await client.auth.authorize({ target: { kind: "pick-space" }, scopes });
170
172
 
171
- // RIGHT: auth.request opens the only popup, then getBySlug() hits token cache
172
- await client.auth.request({ scopes, reason });
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.request()` / `client.auth.requestSpace()` / `client.auth.requestCreateSpace()`. A viewer may grant
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
- Both helpers must be called **from a user gesture** (button click). They are
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 (omit spaceId for the App's own Space).
249
- const ok = await client.auth.request({
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
- // the app only learns the pick. Returning viewers silently reuse their last pick.
257
- const { granted, space } = await client.auth.requestSpace({
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 picked = client.space(space.id);
268
+ if (picked.status === "granted" && picked.target.kind === "space") {
269
+ const space = client.space(picked.target.spaceId);
263
270
  }
264
271
 
265
- // Create a viewer-owned Space and grant on it — never silent. `space` is the
266
- // same `CreateSpaceInput` as `client.spaces.create()` (blank, git, or checkpoint).
267
- const created = await client.auth.requestCreateSpace({
268
- scopes: ["file.view", "session.view", "session.prompt.fullaccess"],
269
- space: {
270
- name: "Whale Shrine",
271
- bootstrapSource: { type: "checkpoint", checkpointId },
272
- },
273
- reason: "Create a workspace from this template.",
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
- Pass `alwaysAsk: true` to skip silent reuse and force a fresh dialog — for
283
- re-confirming a grant or letting the viewer switch to another Space.
284
- `requestCreateSpace` always opens the dialog; each confirm mints a new Space.
285
- The host creates it with the viewer's account token. An App can also create a
286
- viewer-owned Space directly with `client.spaces.create()` once it holds a
287
- `space.create` viewer grant.
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.request`'s job.
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.requestSpace` at runtime
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?.space?.id) {
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. Obtain the space client for API calls
470
- const space = client.space(ctx.space.id);
471
-
472
- // 4. Request viewer grants (from a user gesture, e.g. button click)
473
- const ok = await client.auth.request({
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
- // 5. Call capabilities
479
- const result = await space.prompt({ content: [{ type: "text", text: "Hello" }] });
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.request` must be called from a user gesture** (click handler).
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.request({
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.request({
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.request({
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.request({
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?.space?.id) {
1051
+ if (!ctx?.app?.id) {
1011
1052
  throw new Error("Not running inside a published app runtime.");
1012
1053
  }
1013
- spaceId = ctx.space.id;
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.request (silent when covered).
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?.space?.id) && g.scopes.includes(scope),
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 ok = await client.auth.request({ scopes, reason });
1036
- log(outEl, ok ? "Authorized." : "Authorization denied.");
1037
- return ok;
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.request must be in a user gesture) ---
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
- "space.id": ctx.space?.id,
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.request()` is called from a user gesture** (button click), not
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 `spaceId` when requesting, or use `auth.requestSpace` to let the
1230
- viewer pick. Use `auth.requestCreateSpace` to mint a new viewer-owned Space
1231
- in one consent, or `spaces.create()` directly once the app holds a
1232
- `space.create` viewer grant.
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.request()` before any other
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub",
3
- "version": "8.17.0",
3
+ "version": "8.19.0",
4
4
  "description": "Cohub SDK for spaces, sessions, boards, and realtime agent collaboration.",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,