@neta-art/cohub 8.10.2 → 8.11.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/index.js CHANGED
@@ -891,14 +891,14 @@ var GenerationPolicyError = class extends Error {
891
891
  this.name = "GenerationPolicyError";
892
892
  }
893
893
  };
894
- function isRecord$2(value) {
894
+ function isRecord$4(value) {
895
895
  return !!value && typeof value === "object" && !Array.isArray(value);
896
896
  }
897
897
  function isPrimitiveEnumValue(value) {
898
898
  return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
899
899
  }
900
900
  function normalizeConstraint(value) {
901
- if (!isRecord$2(value) || typeof value.kind !== "string") return null;
901
+ if (!isRecord$4(value) || typeof value.kind !== "string") return null;
902
902
  if (value.kind === "enum") {
903
903
  if (!Array.isArray(value.values) || value.values.length === 0 || !value.values.every(isPrimitiveEnumValue)) return null;
904
904
  return {
@@ -928,7 +928,7 @@ function normalizeConstraint(value) {
928
928
  return null;
929
929
  }
930
930
  function normalizeGenerationPolicy(value) {
931
- if (!isRecord$2(value) || value.version !== 1) return null;
931
+ if (!isRecord$4(value) || value.version !== 1) return null;
932
932
  if (value.mode === "auto") return {
933
933
  version: 1,
934
934
  mode: "auto"
@@ -936,10 +936,10 @@ function normalizeGenerationPolicy(value) {
936
936
  if (value.mode !== "limited" || !Array.isArray(value.models) || value.models.length === 0) return null;
937
937
  const models = [];
938
938
  for (const item of value.models) {
939
- if (!isRecord$2(item) || typeof item.model !== "string" || !item.model.trim()) return null;
939
+ if (!isRecord$4(item) || typeof item.model !== "string" || !item.model.trim()) return null;
940
940
  const modelPolicy = { model: item.model.trim() };
941
941
  if (item.parameters !== void 0) {
942
- if (!isRecord$2(item.parameters)) return null;
942
+ if (!isRecord$4(item.parameters)) return null;
943
943
  const parameters = {};
944
944
  for (const [key, rawConstraint] of Object.entries(item.parameters)) {
945
945
  if (!key.trim()) return null;
@@ -1172,8 +1172,8 @@ const APP_SURFACE_REQUEST_TIMEOUT_MS = 15e3;
1172
1172
  const APP_COMPOSER_CHIP_KEY_MAX_LENGTH = 80;
1173
1173
  const APP_COMPOSER_CHIP_LABEL_MAX_LENGTH = 120;
1174
1174
  const APP_COMPOSER_CHIP_CONTENT_MAX_BYTES = 32768;
1175
- const isRecord$1 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
1176
- const isSurfaceEnvelope = (value) => isRecord$1(value) && value.protocol === "cohub.app.surface" && value.version === 1;
1175
+ const isRecord$3 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
1176
+ const isSurfaceEnvelope = (value) => isRecord$3(value) && value.protocol === "cohub.app.surface" && value.version === 1;
1177
1177
  const parseAppSurfaceReady = (value) => {
1178
1178
  if (!isSurfaceEnvelope(value) || value.type !== "ready") return null;
1179
1179
  const methods = Array.isArray(value.methods) ? value.methods.filter((method) => typeof method === "string" && Boolean(method)) : [];
@@ -1187,7 +1187,7 @@ const parseAppSurfaceReady = (value) => {
1187
1187
  const parseAppSurfaceResponse = (value) => {
1188
1188
  if (!isSurfaceEnvelope(value) || value.type !== "response") return null;
1189
1189
  if (typeof value.requestId !== "string" || !value.requestId) return null;
1190
- const error = isRecord$1(value.error) ? {
1190
+ const error = isRecord$3(value.error) ? {
1191
1191
  code: typeof value.error.code === "string" && value.error.code ? value.error.code : "surface_error",
1192
1192
  message: typeof value.error.message === "string" ? value.error.message : "App surface call failed"
1193
1193
  } : void 0;
@@ -1207,7 +1207,7 @@ const parseComposerChipKey = (value) => {
1207
1207
  return key;
1208
1208
  };
1209
1209
  const parseAppComposerChipSet = (value) => {
1210
- if (!isSurfaceEnvelope(value) || value.type !== "composer.chip.set" || !isRecord$1(value.chip)) return null;
1210
+ if (!isSurfaceEnvelope(value) || value.type !== "composer.chip.set" || !isRecord$3(value.chip)) return null;
1211
1211
  const key = parseComposerChipKey(value.chip.key);
1212
1212
  if (!key || typeof value.chip.label !== "string" || typeof value.chip.content !== "string") return null;
1213
1213
  const label = value.chip.label.trim();
@@ -1289,12 +1289,17 @@ const buildAppRuntimeReady = () => ({
1289
1289
  version: 1,
1290
1290
  type: "ready"
1291
1291
  });
1292
+ const buildAppRuntimeCloseRequest = () => ({
1293
+ protocol: APP_RUNTIME_PROTOCOL,
1294
+ version: 1,
1295
+ type: "close.request"
1296
+ });
1292
1297
  //#endregion
1293
1298
  //#region ../protocol/dist/app-navigation.js
1294
1299
  const APP_NAVIGATION_PROTOCOL = "cohub.app.navigation";
1295
1300
  const APP_NAVIGATION_MAX_ERROR_CODE_LENGTH = 64;
1296
1301
  const APP_NAVIGATION_MAX_ERROR_MESSAGE_LENGTH = NAVIGATION_ERROR_MESSAGE_MAX_LENGTH;
1297
- const isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
1302
+ const isRecord$2 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
1298
1303
  const text = (value, max) => typeof value === "string" && value.trim() && value.length <= max ? value : null;
1299
1304
  const buildAppNavigationOpenMessage = (input) => ({
1300
1305
  protocol: APP_NAVIGATION_PROTOCOL,
@@ -1310,7 +1315,7 @@ const NAVIGATION_REASONS = /* @__PURE__ */ new Set([
1310
1315
  ]);
1311
1316
  function parseNavigationCall(value) {
1312
1317
  if (value === void 0) return void 0;
1313
- if (!isRecord(value) || typeof value.ok !== "boolean") return null;
1318
+ if (!isRecord$2(value) || typeof value.ok !== "boolean") return null;
1314
1319
  if (value.ok) return {
1315
1320
  ok: true,
1316
1321
  ...value.result === void 0 ? {} : { result: value.result }
@@ -1325,7 +1330,7 @@ function parseNavigationCall(value) {
1325
1330
  };
1326
1331
  }
1327
1332
  const parseAppNavigationOpenResponse = (value) => {
1328
- if (!isRecord(value)) return null;
1333
+ if (!isRecord$2(value)) return null;
1329
1334
  if (value.protocol !== "cohub.app.navigation" || value.version !== 1 || value.type !== "open.result" || !text(value.requestId, 128) || typeof value.handled !== "boolean" || value.reason !== void 0 && !NAVIGATION_REASONS.has(value.reason)) return null;
1330
1335
  const call = parseNavigationCall(value.call);
1331
1336
  if (call === null) return null;
@@ -1339,6 +1344,39 @@ const parseAppNavigationOpenResponse = (value) => {
1339
1344
  ...call === void 0 ? {} : { call }
1340
1345
  };
1341
1346
  };
1347
+ const envelope = {
1348
+ protocol: "cohub.app.embed",
1349
+ version: 1
1350
+ };
1351
+ const isRecord$1 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
1352
+ const isEnvelope = (value) => isRecord$1(value) && value.protocol === "cohub.app.embed" && value.version === 1;
1353
+ const parseEmbedId = (value) => typeof value === "string" && value.trim() && value.length <= 128 ? value : null;
1354
+ function parseAppEmbedAttachRequest(value) {
1355
+ if (!isEnvelope(value) || value.type !== "attach.request") return null;
1356
+ return {
1357
+ ...envelope,
1358
+ type: "attach.request"
1359
+ };
1360
+ }
1361
+ function parseAppEmbedCloseRequest(value) {
1362
+ if (!isEnvelope(value) || value.type !== "close.request") return null;
1363
+ const embedId = parseEmbedId(value.embedId);
1364
+ return embedId ? {
1365
+ ...envelope,
1366
+ type: "close.request",
1367
+ embedId
1368
+ } : null;
1369
+ }
1370
+ const buildAppEmbedAttach = (input) => ({
1371
+ ...envelope,
1372
+ type: "attach",
1373
+ ...input
1374
+ });
1375
+ const buildAppEmbedShellChanged = (input) => ({
1376
+ ...envelope,
1377
+ type: "shell.changed",
1378
+ ...input
1379
+ });
1342
1380
  //#endregion
1343
1381
  //#region src/apis/billing.ts
1344
1382
  var BillingApi = class {
@@ -1988,6 +2026,61 @@ var AppSurfaceApi = class {
1988
2026
  /** @deprecated Use `AppSurfaceApi`. */
1989
2027
  var WorkSurfaceApi = class extends AppSurfaceApi {};
1990
2028
  //#endregion
2029
+ //#region src/app-embed.ts
2030
+ const generateEmbedId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
2031
+ const frameOrigin = (frame) => {
2032
+ try {
2033
+ return new URL(frame.src, window.location.href).origin;
2034
+ } catch {
2035
+ return null;
2036
+ }
2037
+ };
2038
+ /**
2039
+ * Attaches to an iframe that renders a Cohub public App page. The page keeps
2040
+ * owning the embedded App's runtime; this only forwards navigation hints and
2041
+ * relays the App's close intent back to the embedder. Either side may come up
2042
+ * first: the page asks to be attached, and the embedder also announces itself
2043
+ * on attach and on every frame load.
2044
+ */
2045
+ function attachAppEmbed(frame, options) {
2046
+ const embedId = generateEmbedId();
2047
+ let shell = options.shell ?? null;
2048
+ const post = (message) => {
2049
+ const origin = frameOrigin(frame);
2050
+ if (!origin) return;
2051
+ try {
2052
+ frame.contentWindow?.postMessage(message, origin);
2053
+ } catch {}
2054
+ };
2055
+ const attach = () => post(buildAppEmbedAttach({
2056
+ embedId,
2057
+ embedder: { appId: options.appId },
2058
+ shell
2059
+ }));
2060
+ const onMessage = (event) => {
2061
+ if (event.source !== frame.contentWindow || event.origin !== frameOrigin(frame)) return;
2062
+ if (parseAppEmbedAttachRequest(event.data)) return attach();
2063
+ if (parseAppEmbedCloseRequest(event.data)?.embedId === embedId) options.onCloseRequest?.();
2064
+ };
2065
+ window.addEventListener("message", onMessage);
2066
+ frame.addEventListener("load", attach);
2067
+ attach();
2068
+ return {
2069
+ embedId,
2070
+ setShell(next) {
2071
+ shell = next;
2072
+ post(buildAppEmbedShellChanged({
2073
+ embedId,
2074
+ shell
2075
+ }));
2076
+ },
2077
+ dispose() {
2078
+ window.removeEventListener("message", onMessage);
2079
+ frame.removeEventListener("load", attach);
2080
+ }
2081
+ };
2082
+ }
2083
+ //#endregion
1991
2084
  //#region src/app-runtime.ts
1992
2085
  const isBrowser$1 = () => typeof window !== "undefined" && typeof window.parent !== "undefined";
1993
2086
  const hasParent = () => isBrowser$1() && window.parent !== window;
@@ -2038,6 +2131,12 @@ var ParentBridgeTransport = class {
2038
2131
  }
2039
2132
  };
2040
2133
  }
2134
+ notify(message) {
2135
+ if (!hasParent()) return;
2136
+ try {
2137
+ window.parent.postMessage(message, this.trustedParentOrigin ?? getParentOrigin() ?? "*");
2138
+ } catch {}
2139
+ }
2041
2140
  request(message, options) {
2042
2141
  const timeoutMs = options?.timeoutMs ?? 1200;
2043
2142
  const retryIntervalMs = options?.retryIntervalMs;
@@ -2345,6 +2444,14 @@ var AppRuntimeApi = class {
2345
2444
  reason: response === null ? "timeout" : "unsupported"
2346
2445
  };
2347
2446
  }
2447
+ /**
2448
+ * Asks the host to close this App: a workspace tab closes, an embedded page
2449
+ * forwards the request to its embedder, a standalone page closes the tab.
2450
+ * No-op in broker mode, where the App owns its own window.
2451
+ */
2452
+ requestClose() {
2453
+ this.transport.notify?.(buildAppRuntimeCloseRequest());
2454
+ }
2348
2455
  async getAccessToken(options) {
2349
2456
  await this.ensureStorageKeys();
2350
2457
  if (this.token && !options?.forceRefresh) return this.token;
@@ -2442,6 +2549,41 @@ var AppRuntimeApi = class {
2442
2549
  } : null
2443
2550
  };
2444
2551
  }
2552
+ /**
2553
+ * One consent: create a viewer-owned Space (full `CreateSpaceInput`, same
2554
+ * as `spaces.create`) and grant the scopes on it. Never silent — each
2555
+ * confirm mints a new Space. The host creates with the viewer's account
2556
+ * token; the app never calls `POST /api/spaces` itself.
2557
+ * `{ granted: false, space }` means the Space was created but not provisioned;
2558
+ * no grant was issued. A viewer deny is `{ granted: false, space: null }`.
2559
+ */
2560
+ async requestCreateSpaceAuthorization(input) {
2561
+ await this.ensureStorageKeys();
2562
+ const response = await this.transport.request({
2563
+ type: "cohub.app.authorize",
2564
+ scopes: input.scopes,
2565
+ reason: input.reason,
2566
+ createSpace: input.space
2567
+ }, { timeoutMs: 12e4 });
2568
+ const token = response?.token ?? null;
2569
+ const spaceId = typeof response?.space?.id === "string" ? response.space.id : null;
2570
+ const spaceName = typeof response?.space?.name === "string" ? response.space.name : null;
2571
+ if (token) {
2572
+ this.token = token;
2573
+ this.writeStoredToken(token);
2574
+ }
2575
+ if (token && spaceId) {
2576
+ this.authorizedGrants = recordConsent(this.authorizedGrants, spaceId, input.scopes);
2577
+ this.writeStoredGrants(this.authorizedGrants);
2578
+ }
2579
+ return {
2580
+ granted: Boolean(token),
2581
+ space: spaceId ? {
2582
+ id: spaceId,
2583
+ name: spaceName
2584
+ } : null
2585
+ };
2586
+ }
2445
2587
  async purchase(input) {
2446
2588
  const purchaseAttemptId = input.purchaseAttemptId?.trim() || generateRequestId();
2447
2589
  return (await this.transport.request({
@@ -2613,13 +2755,20 @@ var CohubClient = class {
2613
2755
  /** Ensure the app holds these scopes. Silent when a grant already covers them; `alwaysAsk` forces the dialog. */
2614
2756
  request: (input) => this.appRuntime.requestAuthorization(input),
2615
2757
  /** One consent: the viewer picks a Space and grants the scopes on it. `alwaysAsk` re-opens the picker. */
2616
- requestSpace: (input) => this.appRuntime.requestSpaceAuthorization(input)
2758
+ requestSpace: (input) => this.appRuntime.requestSpaceAuthorization(input),
2759
+ /** One consent: create a viewer-owned Space and grant the scopes on it. `space` is `CreateSpaceInput`. */
2760
+ requestCreateSpace: (input) => this.appRuntime.requestCreateSpaceAuthorization(input)
2617
2761
  };
2618
2762
  app = {
2619
2763
  realtime: null,
2620
2764
  /** Expose callable methods from inside a published app. */
2621
2765
  surface: new AppSurfaceApi(),
2622
2766
  onContextChanged: (listener) => this.appRuntime.onContextChanged(listener),
2767
+ /** Ask the host to close this App's surface. */
2768
+ requestClose: () => this.appRuntime.requestClose(),
2769
+ embed: {
2770
+ /** Host another App's public page in an iframe and forward the shell location to it. */
2771
+ attach: attachAppEmbed },
2623
2772
  composer: {
2624
2773
  /** Attach or update context from this app in the Cohub composer. */
2625
2774
  setChip: (chip) => this.app.surface.setComposerChip(chip),
@@ -2987,6 +3136,88 @@ function sanitizeReason(value) {
2987
3136
  if (!trimmed) return void 0;
2988
3137
  return trimmed.length > MAX_REASON_LENGTH ? trimmed.slice(0, MAX_REASON_LENGTH) : trimmed;
2989
3138
  }
3139
+ function isRecord(value) {
3140
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
3141
+ }
3142
+ function sanitizeBootstrapSource(value) {
3143
+ if (!isRecord(value) || typeof value.type !== "string") return null;
3144
+ if (value.type === "blank") return { type: "blank" };
3145
+ if (value.type === "checkpoint") {
3146
+ const checkpointId = typeof value.checkpointId === "string" ? value.checkpointId.trim() : "";
3147
+ return checkpointId ? {
3148
+ type: "checkpoint",
3149
+ checkpointId
3150
+ } : null;
3151
+ }
3152
+ if (value.type === "git_repo") {
3153
+ const repoUrl = typeof value.repoUrl === "string" ? value.repoUrl.trim() : "";
3154
+ if (!repoUrl) return null;
3155
+ const ref = typeof value.ref === "string" ? value.ref.trim() || null : value.ref === null ? null : void 0;
3156
+ return ref === void 0 ? {
3157
+ type: "git_repo",
3158
+ repoUrl
3159
+ } : {
3160
+ type: "git_repo",
3161
+ repoUrl,
3162
+ ref
3163
+ };
3164
+ }
3165
+ return null;
3166
+ }
3167
+ /**
3168
+ * Untrusted postMessage payload → `CreateSpaceInput`. Unknown keys are
3169
+ * dropped; the create API still validates the rest.
3170
+ */
3171
+ function sanitizeCreateSpaceInput(value) {
3172
+ if (!isRecord(value)) return null;
3173
+ const name = typeof value.name === "string" ? value.name.trim() : "";
3174
+ if (!name) return null;
3175
+ const input = { name };
3176
+ if (value.slug !== void 0) {
3177
+ if (value.slug !== null && typeof value.slug !== "string") return null;
3178
+ input.slug = value.slug;
3179
+ }
3180
+ if (value.description !== void 0) {
3181
+ if (value.description !== null && typeof value.description !== "string") return null;
3182
+ input.description = value.description;
3183
+ }
3184
+ if (value.source !== void 0) {
3185
+ if (typeof value.source !== "string") return null;
3186
+ input.source = value.source;
3187
+ }
3188
+ if (value.bootstrapSource !== void 0) {
3189
+ const bootstrap = sanitizeBootstrapSource(value.bootstrapSource);
3190
+ if (!bootstrap) return null;
3191
+ input.bootstrapSource = bootstrap;
3192
+ }
3193
+ if (value.extraEnv !== void 0) {
3194
+ if (!Array.isArray(value.extraEnv)) return null;
3195
+ input.extraEnv = value.extraEnv;
3196
+ }
3197
+ if (value.channelBindings !== void 0) {
3198
+ if (!Array.isArray(value.channelBindings)) return null;
3199
+ input.channelBindings = value.channelBindings;
3200
+ }
3201
+ if (value.mods !== void 0) {
3202
+ if (!Array.isArray(value.mods)) return null;
3203
+ input.mods = value.mods;
3204
+ }
3205
+ if (value.config !== void 0) {
3206
+ if (!isRecord(value.config)) return null;
3207
+ input.config = value.config;
3208
+ }
3209
+ return input;
3210
+ }
3211
+ function readCreatedSpace(payload) {
3212
+ if (!isRecord(payload) || !isRecord(payload.space)) return null;
3213
+ const id = payload.space.id;
3214
+ if (typeof id !== "string" || !id) return null;
3215
+ const name = payload.space.name;
3216
+ return {
3217
+ id,
3218
+ name: typeof name === "string" && name ? name : null
3219
+ };
3220
+ }
2990
3221
  function normalizePermissionScopes(scopes) {
2991
3222
  return Array.from(new Set(clonePermissionScopes(scopes)));
2992
3223
  }
@@ -3108,6 +3339,8 @@ function createAppBridgeCore(config) {
3108
3339
  const pendingPurchaseStorageKey = `cohub-app-purchase:${app.id}`;
3109
3340
  const purchaseInFlight = /* @__PURE__ */ new Map();
3110
3341
  let activePurchase = null;
3342
+ /** Space minted during the current create-space consent; retries skip create. */
3343
+ let mintedSpace = null;
3111
3344
  async function isCurrentViewerAppOwner() {
3112
3345
  const viewerUuid = await getViewerUuid();
3113
3346
  return Boolean(viewerUuid && viewerUuid === app.userUuid);
@@ -3225,6 +3458,45 @@ function createAppBridgeCore(config) {
3225
3458
  } catch {}
3226
3459
  }
3227
3460
  /**
3461
+ * Creates the Space with the viewer's own token — the same `POST /api/spaces`
3462
+ * path as the web New Space flow and the CLI. The app never holds this token.
3463
+ */
3464
+ async function createViewerSpace(input) {
3465
+ const request = async (forceRefresh = false) => {
3466
+ const userToken = await getAccessToken({ forceRefresh });
3467
+ if (!userToken) {
3468
+ await config.requestSignIn(typeof location !== "undefined" ? location.pathname : "/");
3469
+ throw new Error("Sign in is required to create a Space.");
3470
+ }
3471
+ return fetch(`${apiOrigin}/api/spaces`, {
3472
+ method: "POST",
3473
+ headers: {
3474
+ Authorization: `Bearer ${userToken}`,
3475
+ "Content-Type": "application/json"
3476
+ },
3477
+ body: JSON.stringify(input)
3478
+ });
3479
+ };
3480
+ let response = await request();
3481
+ if (response.status === 401) response = await request(true);
3482
+ const payload = await response.json().catch(() => null);
3483
+ const space = readCreatedSpace(payload);
3484
+ if (response.ok) {
3485
+ if (!space) throw new Error("Invalid space create response.");
3486
+ return {
3487
+ ...space,
3488
+ provisioned: true
3489
+ };
3490
+ }
3491
+ const message = isRecord(payload) && typeof payload.message === "string" ? payload.message : "Failed to create Space.";
3492
+ if (space) return {
3493
+ ...space,
3494
+ provisioned: false,
3495
+ error: message
3496
+ };
3497
+ throw new Error(message);
3498
+ }
3499
+ /**
3228
3500
  * Resolves the target space's name for the consent dialog. The host — not
3229
3501
  * the app — resolves it, so the dialog cannot be tricked into labeling a
3230
3502
  * grant with the wrong space.
@@ -3380,6 +3652,7 @@ function createAppBridgeCore(config) {
3380
3652
  const scopes = sanitizeRequestedScopes(data.scopes);
3381
3653
  const spaceId = typeof data.spaceId === "string" && data.spaceId ? data.spaceId : void 0;
3382
3654
  const selectSpace = data.selectSpace === true && !spaceId;
3655
+ const createSpace = data.createSpace === void 0 ? void 0 : sanitizeCreateSpaceInput(data.createSpace);
3383
3656
  const alwaysAsk = data.alwaysAsk === true;
3384
3657
  if (scopes.length === 0) {
3385
3658
  replyForRequest(data.requestId, {
@@ -3388,6 +3661,43 @@ function createAppBridgeCore(config) {
3388
3661
  }, true);
3389
3662
  return;
3390
3663
  }
3664
+ if (data.createSpace !== void 0 && !createSpace) {
3665
+ const named = isRecord(data.createSpace) && typeof data.createSpace.name === "string" && Boolean(data.createSpace.name.trim());
3666
+ replyForRequest(data.requestId, {
3667
+ type: "cohub.app.error",
3668
+ message: named ? "Invalid space create input." : "Space name is required."
3669
+ }, true);
3670
+ return;
3671
+ }
3672
+ if (createSpace && (selectSpace || spaceId)) {
3673
+ replyForRequest(data.requestId, {
3674
+ type: "cohub.app.error",
3675
+ message: "createSpace cannot be combined with spaceId or selectSpace."
3676
+ }, true);
3677
+ return;
3678
+ }
3679
+ if (state.authSaving) {
3680
+ replyForRequest(data.requestId, {
3681
+ type: "cohub.app.error",
3682
+ message: "Another authorization is already in progress."
3683
+ }, true);
3684
+ return;
3685
+ }
3686
+ if (state.pendingAuth) dismissPendingAuth();
3687
+ if (createSpace) {
3688
+ mintedSpace = null;
3689
+ state.pendingAuth = {
3690
+ requestId: data.requestId,
3691
+ scopes,
3692
+ reason: sanitizeReason(data.reason),
3693
+ homeSpaceName: app.spaceName ?? null,
3694
+ createSpace
3695
+ };
3696
+ state.authError = null;
3697
+ state.authOpen = true;
3698
+ notify();
3699
+ return;
3700
+ }
3391
3701
  if (!selectSpace && !alwaysAsk && allowsOwnerAutoAuthorization() && await isCurrentViewerAppOwner()) try {
3392
3702
  const result = await authorize(scopes, spaceId, { silent: true });
3393
3703
  replyForRequest(data.requestId, authorizeResult(result.token, result.spaceId, result.spaceId === app.spaceId ? app.spaceName ?? null : null), true);
@@ -3443,16 +3753,22 @@ function createAppBridgeCore(config) {
3443
3753
  }, true);
3444
3754
  }
3445
3755
  }
3446
- function cancelAuth() {
3447
- if (state.authSaving) return;
3756
+ function dismissPendingAuth() {
3448
3757
  if (!state.pendingAuth) return;
3449
3758
  replyForRequest(state.pendingAuth.requestId, {
3450
3759
  type: "cohub.app.authorize.result",
3451
3760
  token: null
3452
3761
  }, true);
3453
- state.authOpen = false;
3454
3762
  state.pendingAuth = null;
3763
+ state.authOpen = false;
3455
3764
  state.authError = null;
3765
+ mintedSpace = null;
3766
+ notify();
3767
+ }
3768
+ function cancelAuth() {
3769
+ if (state.authSaving) return;
3770
+ if (!state.pendingAuth) return;
3771
+ dismissPendingAuth();
3456
3772
  state.authSaving = false;
3457
3773
  notify();
3458
3774
  }
@@ -3501,14 +3817,27 @@ function createAppBridgeCore(config) {
3501
3817
  state.authSaving = true;
3502
3818
  notify();
3503
3819
  try {
3504
- const requestedSpaceId = pending.selectSpace ? pickedSpaceId : pending.spaceId;
3820
+ let requestedSpaceId = pending.selectSpace ? pickedSpaceId : pending.spaceId;
3821
+ let spaceName = pending.selectSpace ? pending.spaces?.find((space) => space.id === requestedSpaceId)?.name ?? null : pending.spaceName ?? app.spaceName ?? null;
3822
+ if (pending.createSpace) {
3823
+ mintedSpace ??= await createViewerSpace(pending.createSpace);
3824
+ if (!mintedSpace.provisioned) {
3825
+ replyForRequest(pending.requestId, authorizeResult(null, mintedSpace.id, mintedSpace.name ?? pending.createSpace.name), true);
3826
+ state.authOpen = false;
3827
+ state.pendingAuth = null;
3828
+ mintedSpace = null;
3829
+ return;
3830
+ }
3831
+ requestedSpaceId = mintedSpace.id;
3832
+ spaceName = mintedSpace.name ?? pending.createSpace.name;
3833
+ }
3505
3834
  const result = await authorize(pending.scopes, requestedSpaceId);
3506
3835
  setGrantedAppScopes(await getViewerUuid(), app.id, result.scopes, result.spaceId);
3507
- if (pending.selectSpace) writeLastPickedSpace(result.spaceId);
3508
- const spaceName = pending.selectSpace ? pending.spaces?.find((space) => space.id === result.spaceId)?.name ?? null : pending.spaceName ?? app.spaceName ?? null;
3836
+ if (pending.selectSpace || pending.createSpace) writeLastPickedSpace(result.spaceId);
3509
3837
  replyForRequest(pending.requestId, authorizeResult(result.token, result.spaceId, spaceName), true);
3510
3838
  state.authOpen = false;
3511
3839
  state.pendingAuth = null;
3840
+ mintedSpace = null;
3512
3841
  } catch (error) {
3513
3842
  state.authError = error instanceof Error ? error.message : "Authorization failed.";
3514
3843
  } finally {
@@ -3758,4 +4087,4 @@ function createBoardExtensionRegistry() {
3758
4087
  }
3759
4088
  const BOARD_CHANNELS = BOARD_ANIMATION_CHANNELS;
3760
4089
  //#endregion
3761
- export { APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppSurfaceApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_ANIMATION_CHANNEL_CAPABILITIES, BOARD_CHANNELS, BOARD_COLOR_IDS, BOARD_GEO_KINDS, BillingApi, BoardAuthoringItemSchema, BoardClient, BoardCompositionInputSchema, BoardCompositionSchema, BoardEffectInputSchema, BoardEffectSchema, BoardExtensionRegistry, BoardItemPatchSchema, BoardPlaybackPolicySchema, BoardSemanticCommandSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DEFAULT_BOARD_LIMITS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DesktopCommandsApi, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HttpError, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, ReferencesApi, ReferralsApi, SessionGenerationStreamClient, SessionPatchReducer, SpacePublicFilesApi, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, assertGenerationRequestAllowedByPolicy, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileComposition, composition, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createBoardExtensionRegistry, createCohubClient, createHttpClient, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, findGenerationModelPolicy, formatAppRef, formatWorkRef, getAllowedGenerationModelIds, getCohubContext, hasGrantedAppScopes, hasRequestSourceIdentity, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, normalizeBaseUrl, normalizeGenerationPolicy, normalizeRequestSource, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardCompositionInput, parseBoardEffectInput, parseBoardPlaybackPolicy, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseWorkRef, proceduralClip, readRequestSourceFromEnv, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebsocketUrl, sampleCompositionTracks, sampleEasing, sampleTrack, sanitizeAccessToken, scopeListHasPermission, setGrantedAppScopes, track };
4090
+ export { APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppSurfaceApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_ANIMATION_CHANNEL_CAPABILITIES, BOARD_CHANNELS, BOARD_COLOR_IDS, BOARD_GEO_KINDS, BillingApi, BoardAuthoringItemSchema, BoardClient, BoardCompositionInputSchema, BoardCompositionSchema, BoardEffectInputSchema, BoardEffectSchema, BoardExtensionRegistry, BoardItemPatchSchema, BoardPlaybackPolicySchema, BoardSemanticCommandSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DEFAULT_BOARD_LIMITS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DesktopCommandsApi, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HttpError, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, ReferencesApi, ReferralsApi, SessionGenerationStreamClient, SessionPatchReducer, SpacePublicFilesApi, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, assertGenerationRequestAllowedByPolicy, attachAppEmbed, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileComposition, composition, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createBoardExtensionRegistry, createCohubClient, createHttpClient, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, findGenerationModelPolicy, formatAppRef, formatWorkRef, getAllowedGenerationModelIds, getCohubContext, hasGrantedAppScopes, hasRequestSourceIdentity, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, normalizeBaseUrl, normalizeGenerationPolicy, normalizeRequestSource, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardCompositionInput, parseBoardEffectInput, parseBoardPlaybackPolicy, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseWorkRef, proceduralClip, readRequestSourceFromEnv, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebsocketUrl, sampleCompositionTracks, sampleEasing, sampleTrack, sanitizeAccessToken, scopeListHasPermission, setGrantedAppScopes, track };
@@ -61,13 +61,14 @@ requiring the viewer to paste an API key.
61
61
  └─────────────────────────────────────────┘
62
62
  ```
63
63
 
64
- Five runtime-only APIs form the foundation; everything else is standard SDK:
64
+ These runtime-only APIs form the foundation; everything else is standard SDK:
65
65
 
66
66
  | API | What it does | Returns |
67
67
  |---|---|---|
68
68
  | `client.context()` | Asks the host for the App's identity | `{ app, space, viewer?, invocation?, permissions }` or `null` |
69
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
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 }` |
71
72
  | `client.context().permissions.viewerGrants` | Render the viewer's current per-space grants | `{ spaceId, scopes }[]` |
72
73
  | `client.app.commerce.*` / `client.app.realtime.*` | Commerce and realtime, bound to the app's runtime identity | (see below) |
73
74
 
@@ -206,7 +207,7 @@ command.execute — run sandbox shell commands
206
207
  ### Viewer grants (consent-required, any permission, per Space)
207
208
 
208
209
  A viewer grants these through a consent dialog triggered by
209
- `client.auth.request()` / `client.auth.requestSpace()`. A viewer may grant
210
+ `client.auth.request()` / `client.auth.requestSpace()` / `client.auth.requestCreateSpace()`. A viewer may grant
210
211
  **any** permission they currently hold on the target Space — including scopes
211
212
  outside the eight app scopes, such as `generation.create` or the account-level
212
213
  `user.*` scopes. Two hard rules are enforced by the server:
@@ -260,10 +261,29 @@ const { granted, space } = await client.auth.requestSpace({
260
261
  if (granted && space) {
261
262
  const picked = client.space(space.id);
262
263
  }
264
+
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.",
274
+ });
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 }`.
263
280
  ```
264
281
 
265
282
  Pass `alwaysAsk: true` to skip silent reuse and force a fresh dialog — for
266
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 — the app never calls
286
+ `POST /api/spaces` itself.
267
287
 
268
288
  ### Checking grant state at runtime
269
289
 
@@ -318,6 +338,7 @@ dialog can.
318
338
  | Read task run detail | `client.tasks.get(taskRunId)` | `taskrun.view` | app or viewer |
319
339
  | List tasks in a Space | `client.tasks.list({ spaceId })` | `taskrun.view` on that Space | app or viewer |
320
340
  | List all owned task runs | `client.tasks.list()` | `user.taskrun.list` | **viewer only** |
341
+ | Create a Space for the viewer | `client.auth.requestCreateSpace({ space, scopes })` | requested scopes on the new Space | **viewer consent** |
321
342
  | List viewer's spaces | `client.spaces.list()` | `user.space.list` | **viewer only** |
322
343
  | List viewer's sessions | `client.user.listSessions()` | `user.session.list` | **viewer only** |
323
344
  | Read viewer's activity | `client.user.getActivity()` | `user.usage.read` | **viewer only** |
@@ -1204,7 +1225,8 @@ Before publishing your App, verify each item:
1204
1225
  on page load. It is safe to call repeatedly — covered scopes renew silently.
1205
1226
  - [ ] **Cross-Space access targets the right Space** — viewer grants are per
1206
1227
  Space. Pass `spaceId` when requesting, or use `auth.requestSpace` to let the
1207
- viewer pick.
1228
+ viewer pick. Use `auth.requestCreateSpace` when the app should mint a new
1229
+ viewer-owned Space; do not call `spaces.create()` with the App token.
1208
1230
  - [ ] **`subscribeGeneration` errors are not silently swallowed** — if the
1209
1231
  stream fails, surface it; a silent fallback to polling will also 403 if
1210
1232
  `session.view` is missing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub",
3
- "version": "8.10.2",
3
+ "version": "8.11.0",
4
4
  "description": "Cohub SDK for spaces, sessions, boards, and realtime agent collaboration.",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,