@neta-art/cohub 8.13.0 → 8.15.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/chunks/environment.d.ts +12 -3
- package/dist/chunks/environment.js +15 -1
- package/dist/chunks/http.d.ts +73 -8
- package/dist/chunks/http.js +1 -0
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/http.d.ts +3 -3
- package/dist/http.js +2 -2
- package/dist/index.d.ts +36 -6
- package/dist/index.js +471 -82
- package/dist/space-picker.d.ts +27 -0
- package/dist/space-picker.js +35 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { $ as BoardConnectionSchema, A as DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, B as BoardItemPatchSchema, C as SessionPatchReducer, D as DESKTOP_COMMAND_MAX_TIMEOUT_MS, E as DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, F as parseDesktopCommand, G as parseBoardEffectInput, H as BoardSemanticMutationSchema, I as parseDesktopCommandId, J as BoardCompositionInputSchema, K as BOARD_ANIMATION_CHANNELS, L as resolveOpenSurface, M as DESKTOP_COMMAND_VERSION, N as isDesktopCallMethod, O as DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, P as isTerminalDesktopCommandStatus, Q as parseBoardCompositionInput, R as NAVIGATION_ERROR_MESSAGE_MAX_LENGTH, S as parseAssistantMessageCommit, T as ensureRealtimeConnected, U as BoardEffectInputSchema, V as BoardSemanticCommandSchema, W as BoardEffectSchema, X as BoardProceduralClipSchema, Y as BoardCompositionSchema, Z as BoardTrackSchema, _ as PublicInviteApi, a as DesktopCommandsApi, at as SessionAccessApi, b as SessionGenerationStreamClient, c as ReferralsApi, ct as PublicAssetsApi, d as TasksApi, dt as ModelsApi, et as BOARD_ARROW_STROKE_SIZE, f as BoardClient, ft as GenerationsApi, g as SpacesApi, h as SpaceWebhooksApi, i as WorkCommerceApi, it as DEFAULT_BOARD_RENDER_LIMITS, j as DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, k as DESKTOP_COMMAND_PENDING_TTL_SECONDS, l as UsersApi, lt as SkillsApi, m as SpacePublicFilesApi, mt as ChannelsApi, n as createHttpClient, nt as BOARD_BUILTIN_CLIP_KINDS, o as UiCommandsApi, ot as ReferencesApi, p as SpaceClient, pt as CronJobsApi, q as BOARD_ANIMATION_CHANNEL_CAPABILITIES, r as AppCommerceApi, rt as BOARD_BUILTIN_EFFECT_KINDS, s as AppsApi, st as SearchApi, t as CohubHttpClient, tt as BOARD_BUILTIN_CAPABILITIES, u as UserApi, ut as PromptsApi, v as buildSpaceInvitePath, w as createSessionPatchReducer, x as createSessionGenerationStreamClient, y as buildSpacePath, z as BoardAuthoringItemSchema } from "./chunks/http.js";
|
|
2
2
|
import { _ as requestSourceToHeaders, a as sanitizeAccessToken, b as REALTIME_ROOM_EVENT_NAME_PATTERN, c as REQUEST_SOURCE_VIA_MAX_LENGTH, d as isRequestSourceEmpty, f as isRequestSourceUuid, g as readRequestSourceFromEnv, h as parseRequestSourceFromHeaders, i as matchesUnauthorizedErrorToken, l as hasRequestSourceIdentity, m as normalizeRequestSource, n as HttpTransport, o as COHUB_SOURCE_HEADER, p as mergeRequestSourceIntoMeta, r as joinApiUrl, s as COHUB_SOURCE_HEADER_NAMES, t as HttpError, u as isRequestSourceClientId, v as resolveRequestSourceChannel, y as isUuid } from "./chunks/transport.js";
|
|
3
|
-
import { a as normalizeWebsocketUrl, c as resolveExecutionAppId, d as resolveWebsocketUrl, f as PERMISSIONS, i as normalizeVoiceInputWebsocketUrl, l as resolveExecutionToken, n as getCohubContext, o as resolveApiBaseUrl, p as
|
|
3
|
+
import { a as normalizeWebsocketUrl, c as resolveExecutionAppId, d as resolveWebsocketUrl, f as PERMISSIONS, h as scopeListHasPermission, i as normalizeVoiceInputWebsocketUrl, l as resolveExecutionToken, m as isUserLevelPermission, n as getCohubContext, o as resolveApiBaseUrl, p as USER_LEVEL_PERMISSIONS, r as normalizeBaseUrl, s as resolveCohubEnvironment, t as COHUB_ENVIRONMENTS, u as resolveVoiceInputWebsocketUrl } from "./chunks/environment.js";
|
|
4
4
|
import { a as extractBillingPayload, c as isFeatureNotEntitledError, i as FEATURE_NOT_ENTITLED_ERROR_CODE, l as isHttpErrorCode, n as createWebsocketClient, o as isBillingAccessBlockedCode, r as BILLING_ACCESS_BLOCKED_ERROR_CODE, s as isBillingAccessBlockedError, t as WebsocketClient } from "./chunks/websocket.js";
|
|
5
5
|
import { VoiceApi, VoiceInputClient, createVoiceInputClient } from "./voice-input.js";
|
|
6
|
+
import { filterSpacePickerItems, normalizeSpacePickerQuery, orderSpacePickerItems, selectSpacePickerItems } from "./space-picker.js";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
//#region ../protocol/dist/board.js
|
|
8
9
|
const BOARD_DOCUMENT_KIND = "cohub.board";
|
|
@@ -1390,6 +1391,14 @@ const buildAppRuntimeConfigureRequest = (input) => ({
|
|
|
1390
1391
|
type: "configure.request",
|
|
1391
1392
|
...input
|
|
1392
1393
|
});
|
|
1394
|
+
const buildAppRuntimePointer = (input) => ({
|
|
1395
|
+
protocol: APP_RUNTIME_PROTOCOL,
|
|
1396
|
+
version: 1,
|
|
1397
|
+
type: "pointer",
|
|
1398
|
+
x: input.x,
|
|
1399
|
+
y: input.y,
|
|
1400
|
+
down: input.down
|
|
1401
|
+
});
|
|
1393
1402
|
//#endregion
|
|
1394
1403
|
//#region ../protocol/dist/app-navigation.js
|
|
1395
1404
|
const APP_NAVIGATION_PROTOCOL = "cohub.app.navigation";
|
|
@@ -2199,8 +2208,55 @@ const generateRequestId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now
|
|
|
2199
2208
|
var ParentBridgeTransport = class {
|
|
2200
2209
|
supportsNavigation = true;
|
|
2201
2210
|
trustedParentOrigin = null;
|
|
2211
|
+
readyAnnounced = false;
|
|
2202
2212
|
contextListeners = /* @__PURE__ */ new Set();
|
|
2203
2213
|
contextListener = null;
|
|
2214
|
+
diagnosticListeners = /* @__PURE__ */ new Set();
|
|
2215
|
+
diagnosticListener = null;
|
|
2216
|
+
/**
|
|
2217
|
+
* Tells the host the runtime is ready once, before any subscription. Hosts
|
|
2218
|
+
* gate unsolicited messages (context pushes, diagnostics) on this handshake,
|
|
2219
|
+
* so the diagnostic subscription must trigger it too — an app that never
|
|
2220
|
+
* subscribes to context still needs diagnostics to arrive.
|
|
2221
|
+
*/
|
|
2222
|
+
announceReady() {
|
|
2223
|
+
if (this.readyAnnounced || !hasParent()) return;
|
|
2224
|
+
const parentOrigin = this.trustedParentOrigin ?? getParentOrigin();
|
|
2225
|
+
if (!parentOrigin) return;
|
|
2226
|
+
try {
|
|
2227
|
+
window.parent.postMessage(buildAppRuntimeReady(), parentOrigin);
|
|
2228
|
+
this.readyAnnounced = true;
|
|
2229
|
+
} catch {}
|
|
2230
|
+
}
|
|
2231
|
+
subscribeDiagnostics(listener) {
|
|
2232
|
+
if (!hasParent() || typeof window.addEventListener !== "function") return () => {};
|
|
2233
|
+
this.diagnosticListeners.add(listener);
|
|
2234
|
+
this.announceReady();
|
|
2235
|
+
if (!this.diagnosticListener) {
|
|
2236
|
+
this.diagnosticListener = (event) => {
|
|
2237
|
+
if (event.source !== window.parent) return;
|
|
2238
|
+
const parentOrigin = this.trustedParentOrigin ?? getParentOrigin();
|
|
2239
|
+
if (parentOrigin && event.origin !== parentOrigin) return;
|
|
2240
|
+
const data = event.data;
|
|
2241
|
+
if (data?.type !== "cohub.app.diagnostic") return;
|
|
2242
|
+
if (typeof data.code !== "string" || typeof data.message !== "string") return;
|
|
2243
|
+
const diagnostic = {
|
|
2244
|
+
code: data.code,
|
|
2245
|
+
message: data.message,
|
|
2246
|
+
...data.detail && typeof data.detail === "object" ? { detail: data.detail } : {}
|
|
2247
|
+
};
|
|
2248
|
+
for (const current of this.diagnosticListeners) current(diagnostic);
|
|
2249
|
+
};
|
|
2250
|
+
window.addEventListener("message", this.diagnosticListener);
|
|
2251
|
+
}
|
|
2252
|
+
return () => {
|
|
2253
|
+
this.diagnosticListeners.delete(listener);
|
|
2254
|
+
if (this.diagnosticListeners.size === 0 && this.diagnosticListener) {
|
|
2255
|
+
window.removeEventListener("message", this.diagnosticListener);
|
|
2256
|
+
this.diagnosticListener = null;
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2204
2260
|
subscribeContextChanged(listener) {
|
|
2205
2261
|
if (!hasParent()) return () => {};
|
|
2206
2262
|
this.contextListeners.add(listener);
|
|
@@ -2214,10 +2270,7 @@ var ParentBridgeTransport = class {
|
|
|
2214
2270
|
for (const current of this.contextListeners) current(data.context);
|
|
2215
2271
|
};
|
|
2216
2272
|
window.addEventListener("message", this.contextListener);
|
|
2217
|
-
|
|
2218
|
-
if (parentOrigin) try {
|
|
2219
|
-
window.parent.postMessage(buildAppRuntimeReady(), parentOrigin);
|
|
2220
|
-
} catch {}
|
|
2273
|
+
this.announceReady();
|
|
2221
2274
|
}
|
|
2222
2275
|
return () => {
|
|
2223
2276
|
this.contextListeners.delete(listener);
|
|
@@ -2433,9 +2486,88 @@ var AppRuntimeApi = class {
|
|
|
2433
2486
|
* refreshes can re-authorize (preserving viewer grants) instead of falling
|
|
2434
2487
|
* back to a base session token that only carries app-side scopes. */
|
|
2435
2488
|
authorizedGrants = null;
|
|
2489
|
+
/**
|
|
2490
|
+
* Rect input regions are hover-gated by the host, which needs to know when
|
|
2491
|
+
* the pointer leaves them. A cross-origin frame swallows those moves once
|
|
2492
|
+
* the host makes it interactive, so while a rect region is active we report
|
|
2493
|
+
* the pointer ourselves. `all`/`none` need no reporting: their state is
|
|
2494
|
+
* known without it.
|
|
2495
|
+
*/
|
|
2496
|
+
pointerListening = false;
|
|
2497
|
+
pointerForwarding = false;
|
|
2498
|
+
pointerDown = false;
|
|
2499
|
+
pointerFrame = null;
|
|
2500
|
+
pointerPending = null;
|
|
2501
|
+
/** Cleanup for the constructor's diagnostic subscription; cleared on dispose. */
|
|
2502
|
+
diagnosticUnsubscribe;
|
|
2503
|
+
onPointerMove = (event) => {
|
|
2504
|
+
if (!this.pointerForwarding) return;
|
|
2505
|
+
this.pointerDown = event.buttons !== 0;
|
|
2506
|
+
this.queuePointer(event.clientX, event.clientY);
|
|
2507
|
+
};
|
|
2508
|
+
onPointerDown = (event) => {
|
|
2509
|
+
if (!this.pointerForwarding) return;
|
|
2510
|
+
this.pointerDown = true;
|
|
2511
|
+
this.flushPointer(event.clientX, event.clientY);
|
|
2512
|
+
};
|
|
2513
|
+
onPointerUp = (event) => {
|
|
2514
|
+
if (!this.pointerForwarding && !this.pointerDown) return;
|
|
2515
|
+
this.pointerDown = false;
|
|
2516
|
+
this.flushPointer(event.clientX, event.clientY);
|
|
2517
|
+
};
|
|
2518
|
+
onPointerCancel = (event) => {
|
|
2519
|
+
if (!this.pointerForwarding && !this.pointerDown) return;
|
|
2520
|
+
this.pointerDown = false;
|
|
2521
|
+
this.flushPointer(event.clientX, event.clientY);
|
|
2522
|
+
};
|
|
2523
|
+
/** Coalesce moves to one message per frame; presses flush immediately. */
|
|
2524
|
+
queuePointer(x, y) {
|
|
2525
|
+
this.pointerPending = {
|
|
2526
|
+
x,
|
|
2527
|
+
y
|
|
2528
|
+
};
|
|
2529
|
+
if (this.pointerFrame !== null) return;
|
|
2530
|
+
this.pointerFrame = requestAnimationFrame(() => {
|
|
2531
|
+
this.pointerFrame = null;
|
|
2532
|
+
const pending = this.pointerPending;
|
|
2533
|
+
this.pointerPending = null;
|
|
2534
|
+
if (pending && this.pointerForwarding) this.reportPointer(pending.x, pending.y, this.pointerDown);
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
flushPointer(x, y) {
|
|
2538
|
+
if (this.pointerFrame !== null) {
|
|
2539
|
+
cancelAnimationFrame(this.pointerFrame);
|
|
2540
|
+
this.pointerFrame = null;
|
|
2541
|
+
}
|
|
2542
|
+
this.pointerPending = null;
|
|
2543
|
+
this.reportPointer(x, y, this.pointerDown);
|
|
2544
|
+
}
|
|
2545
|
+
reportPointer(x, y, down) {
|
|
2546
|
+
this.transport.notify?.(buildAppRuntimePointer({
|
|
2547
|
+
x,
|
|
2548
|
+
y,
|
|
2549
|
+
down
|
|
2550
|
+
}));
|
|
2551
|
+
}
|
|
2552
|
+
setPointerForwarding(on) {
|
|
2553
|
+
if (typeof window === "undefined" || !this.transport.notify) return;
|
|
2554
|
+
this.pointerForwarding = on;
|
|
2555
|
+
if (!on || this.pointerListening) return;
|
|
2556
|
+
this.pointerListening = true;
|
|
2557
|
+
window.addEventListener("pointermove", this.onPointerMove, {
|
|
2558
|
+
passive: true,
|
|
2559
|
+
capture: true
|
|
2560
|
+
});
|
|
2561
|
+
window.addEventListener("pointerdown", this.onPointerDown, true);
|
|
2562
|
+
window.addEventListener("pointerup", this.onPointerUp, true);
|
|
2563
|
+
window.addEventListener("pointercancel", this.onPointerCancel, true);
|
|
2564
|
+
}
|
|
2436
2565
|
constructor(transport = new ParentBridgeTransport(), appId, appIdResolver) {
|
|
2437
2566
|
this.transport = transport;
|
|
2438
2567
|
this.appIdResolver = appIdResolver;
|
|
2568
|
+
this.diagnosticUnsubscribe = transport.subscribeDiagnostics?.((diagnostic) => {
|
|
2569
|
+
console.warn(`[cohub] ${diagnostic.message} (${diagnostic.code})`, diagnostic.detail ?? {});
|
|
2570
|
+
}) ?? null;
|
|
2439
2571
|
if (appId) {
|
|
2440
2572
|
this.tokenStorageKey = `${TOKEN_STORAGE_PREFIX}:${appId}`;
|
|
2441
2573
|
this.grantsStorageKey = `${AUTHORIZED_GRANTS_STORAGE_PREFIX}:${appId}`;
|
|
@@ -2515,6 +2647,32 @@ var AppRuntimeApi = class {
|
|
|
2515
2647
|
onContextChanged(listener) {
|
|
2516
2648
|
return this.transport.subscribeContextChanged?.(listener) ?? (() => {});
|
|
2517
2649
|
}
|
|
2650
|
+
/** Subscribes to developer-facing host diagnostics. */
|
|
2651
|
+
onDiagnostic(listener) {
|
|
2652
|
+
return this.transport.subscribeDiagnostics?.(listener) ?? (() => {});
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Releases listeners and any pending pointer frame this runtime registered.
|
|
2656
|
+
* Safe to call more than once.
|
|
2657
|
+
*/
|
|
2658
|
+
dispose() {
|
|
2659
|
+
const unsubscribe = this.diagnosticUnsubscribe;
|
|
2660
|
+
this.diagnosticUnsubscribe = null;
|
|
2661
|
+
unsubscribe?.();
|
|
2662
|
+
this.pointerForwarding = false;
|
|
2663
|
+
this.pointerPending = null;
|
|
2664
|
+
if (this.pointerFrame !== null) {
|
|
2665
|
+
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(this.pointerFrame);
|
|
2666
|
+
this.pointerFrame = null;
|
|
2667
|
+
}
|
|
2668
|
+
if (this.pointerListening && typeof window !== "undefined") {
|
|
2669
|
+
window.removeEventListener("pointermove", this.onPointerMove, { capture: true });
|
|
2670
|
+
window.removeEventListener("pointerdown", this.onPointerDown, true);
|
|
2671
|
+
window.removeEventListener("pointerup", this.onPointerUp, true);
|
|
2672
|
+
window.removeEventListener("pointercancel", this.onPointerCancel, true);
|
|
2673
|
+
this.pointerListening = false;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2518
2676
|
async navigationOpen(target, call) {
|
|
2519
2677
|
if (this.transport.supportsNavigation === false) return {
|
|
2520
2678
|
protocol: "cohub.app.navigation",
|
|
@@ -2552,9 +2710,13 @@ var AppRuntimeApi = class {
|
|
|
2552
2710
|
* Requests the host to update the overlay's geometry or pointer hit regions.
|
|
2553
2711
|
* Only meaningful when the App was opened as an `overlay` surface; the host
|
|
2554
2712
|
* is free to clamp or ignore values that violate its layout policy.
|
|
2713
|
+
*
|
|
2714
|
+
* A non-empty rect `inputRegion` also starts reporting the pointer to the
|
|
2715
|
+
* host, which is how the host learns when it leaves the region.
|
|
2555
2716
|
*/
|
|
2556
2717
|
requestConfigure(input) {
|
|
2557
2718
|
this.transport.notify?.(buildAppRuntimeConfigureRequest(input));
|
|
2719
|
+
if (input.inputRegion !== void 0) this.setPointerForwarding(Array.isArray(input.inputRegion) && input.inputRegion.length > 0);
|
|
2558
2720
|
}
|
|
2559
2721
|
async getAccessToken(options) {
|
|
2560
2722
|
await this.ensureStorageKeys();
|
|
@@ -2596,10 +2758,15 @@ var AppRuntimeApi = class {
|
|
|
2596
2758
|
return this.token;
|
|
2597
2759
|
}
|
|
2598
2760
|
/**
|
|
2599
|
-
* Requests viewer consent.
|
|
2600
|
-
*
|
|
2601
|
-
*
|
|
2602
|
-
*
|
|
2761
|
+
* Requests viewer consent. With an accessible `spaceId` the grant targets
|
|
2762
|
+
* that Space; otherwise the host resolves a viewer-controlled Space (the
|
|
2763
|
+
* invocation or embedding Space, then the viewer's last picked Space, then
|
|
2764
|
+
* their first accessible one) — never the app author's home Space. The app
|
|
2765
|
+
* may only grant what the viewer can already do there themselves. Reuses a
|
|
2766
|
+
* previous grant silently unless `alwaysAsk` forces the consent dialog.
|
|
2767
|
+
*
|
|
2768
|
+
* Returns whether a token was granted; use
|
|
2769
|
+
* {@link requestSpaceAuthorization} when the app needs to know which Space.
|
|
2603
2770
|
*/
|
|
2604
2771
|
async requestAuthorization(input) {
|
|
2605
2772
|
await this.ensureStorageKeys();
|
|
@@ -2855,8 +3022,17 @@ var CohubClient = class {
|
|
|
2855
3022
|
context() {
|
|
2856
3023
|
return this.appRuntime.context();
|
|
2857
3024
|
}
|
|
3025
|
+
/** Releases listeners this client registered. Safe to call more than once. */
|
|
3026
|
+
dispose() {
|
|
3027
|
+
this.appRuntime.dispose();
|
|
3028
|
+
}
|
|
2858
3029
|
auth = {
|
|
2859
|
-
/**
|
|
3030
|
+
/**
|
|
3031
|
+
* Ensure the app holds these scopes. Without an accessible `spaceId` the
|
|
3032
|
+
* host targets a viewer-controlled Space (never the app author's home
|
|
3033
|
+
* Space) — use `requestSpace` when the app needs to know which Space.
|
|
3034
|
+
* Silent when a grant already covers them; `alwaysAsk` forces the dialog.
|
|
3035
|
+
*/
|
|
2860
3036
|
request: (input) => this.appRuntime.requestAuthorization(input),
|
|
2861
3037
|
/** One consent: the viewer picks a Space and grants the scopes on it. `alwaysAsk` re-opens the picker. */
|
|
2862
3038
|
requestSpace: (input) => this.appRuntime.requestSpaceAuthorization(input),
|
|
@@ -3122,6 +3298,19 @@ function hasGrantedAppScopes(userUuid, appId, scopes, spaceId) {
|
|
|
3122
3298
|
return scopes.every((scope) => scopeListHasPermission(entry.scopes, scope));
|
|
3123
3299
|
}
|
|
3124
3300
|
/**
|
|
3301
|
+
* The Space ids of cached grants that cover every requested scope, scanning all
|
|
3302
|
+
* Spaces. Used for account-level scopes, which are not bound to one Space but
|
|
3303
|
+
* must be renewed against a Space whose grant actually exists. The coverage is
|
|
3304
|
+
* already validated here, so callers must not re-check with a per-Space key
|
|
3305
|
+
* (a legacy, Space-less entry would otherwise never match).
|
|
3306
|
+
*/
|
|
3307
|
+
function listGrantedAppSpacesForScopes(userUuid, appId, scopes, homeSpaceId) {
|
|
3308
|
+
if (!userUuid || !appId || scopes.length === 0) return [];
|
|
3309
|
+
const spaces = /* @__PURE__ */ new Set();
|
|
3310
|
+
for (const grant of listGrantedAppScopes(userUuid, appId, homeSpaceId)) if (scopes.every((scope) => scopeListHasPermission(grant.scopes, scope))) spaces.add(grant.spaceId);
|
|
3311
|
+
return [...spaces];
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3125
3314
|
* Lists every cached grant for an app — one entry per space — so hosts can
|
|
3126
3315
|
* report what the viewer previously consented to without a server round trip.
|
|
3127
3316
|
* Entries past their re-consent window are dropped on read.
|
|
@@ -3226,11 +3415,24 @@ function sanitizeRequestedScopes(value) {
|
|
|
3226
3415
|
}
|
|
3227
3416
|
/** Consent dialogs render the reason; keep hostile input bounded. */
|
|
3228
3417
|
const MAX_REASON_LENGTH = 280;
|
|
3418
|
+
/**
|
|
3419
|
+
* Bound the viewer's Space lookup so a stalled network can't leave the consent
|
|
3420
|
+
* dialog reserved but invisible until the SDK request times out.
|
|
3421
|
+
*/
|
|
3422
|
+
const SPACE_LIST_TIMEOUT_MS = 1e4;
|
|
3423
|
+
/** Bound a single authorize call so a stalled network can't wedge the flow. */
|
|
3424
|
+
const AUTHORIZE_TIMEOUT_MS = 3e4;
|
|
3425
|
+
/** Cap serial silent-renewal attempts per request (account grants may span Spaces). */
|
|
3426
|
+
const MAX_SILENT_SPACE_ATTEMPTS = 4;
|
|
3427
|
+
const timeoutSignal = (ms) => typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(ms) : void 0;
|
|
3428
|
+
const spaceListSignal = () => timeoutSignal(SPACE_LIST_TIMEOUT_MS);
|
|
3229
3429
|
var AppAuthorizationError = class extends Error {
|
|
3230
3430
|
status;
|
|
3231
|
-
|
|
3431
|
+
code;
|
|
3432
|
+
constructor(message, status, code) {
|
|
3232
3433
|
super(message);
|
|
3233
3434
|
this.status = status;
|
|
3435
|
+
this.code = code;
|
|
3234
3436
|
this.name = "AppAuthorizationError";
|
|
3235
3437
|
}
|
|
3236
3438
|
};
|
|
@@ -3239,6 +3441,17 @@ const isDefinitiveAuthorizationFailure = (error) => error instanceof AppAuthoriz
|
|
|
3239
3441
|
403,
|
|
3240
3442
|
404
|
|
3241
3443
|
].includes(error.status);
|
|
3444
|
+
/** A grant the server rejected (revoked / not found) — safe to drop from cache. */
|
|
3445
|
+
const isGrantRejection = (error) => error instanceof AppAuthorizationError && [403, 404].includes(error.status);
|
|
3446
|
+
/** A stale access token — never a revoked grant, so never clear the cache. */
|
|
3447
|
+
const isAuthFailure = (error) => error instanceof AppAuthorizationError && error.status === 401;
|
|
3448
|
+
/** Viewer-facing copy for grant failures the viewer cannot act on technically. */
|
|
3449
|
+
const VIEWER_OPAQUE_AUTH_MESSAGES = {
|
|
3450
|
+
space_inaccessible: "Couldn't connect this app to your Space. Try another Space.",
|
|
3451
|
+
app_not_accessible: "This app isn't available in this Space right now.",
|
|
3452
|
+
scope_not_held: "This app needs permissions you don't have yet."
|
|
3453
|
+
};
|
|
3454
|
+
const viewerOpaqueAuthMessage = (error) => error instanceof AppAuthorizationError && error.code ? VIEWER_OPAQUE_AUTH_MESSAGES[error.code] ?? null : null;
|
|
3242
3455
|
/**
|
|
3243
3456
|
* Whether a new authorize request asks for exactly the consent a pending
|
|
3244
3457
|
* dialog already shows: same scopes, same target and same mode. Such a request
|
|
@@ -3454,11 +3667,30 @@ function createAppBridgeCore(config) {
|
|
|
3454
3667
|
authOpen: false,
|
|
3455
3668
|
pendingAuth: null,
|
|
3456
3669
|
authError: null,
|
|
3457
|
-
authSaving: false
|
|
3670
|
+
authSaving: false,
|
|
3671
|
+
selectedSpaceId: null,
|
|
3672
|
+
canChangeSpace: false
|
|
3458
3673
|
};
|
|
3459
3674
|
function notify() {
|
|
3460
3675
|
onStateChange?.({ ...state });
|
|
3461
3676
|
}
|
|
3677
|
+
/** notify() that never lets a broken host callback escape. */
|
|
3678
|
+
function safeNotify() {
|
|
3679
|
+
try {
|
|
3680
|
+
notify();
|
|
3681
|
+
} catch {}
|
|
3682
|
+
}
|
|
3683
|
+
/** Sends a developer-facing diagnostic to the app; never surfaced to viewers. */
|
|
3684
|
+
function emitDiagnostic(diagnostic) {
|
|
3685
|
+
try {
|
|
3686
|
+
config.notify?.({
|
|
3687
|
+
type: "cohub.app.diagnostic",
|
|
3688
|
+
code: diagnostic.code,
|
|
3689
|
+
message: diagnostic.message,
|
|
3690
|
+
...diagnostic.detail ? { detail: diagnostic.detail } : {}
|
|
3691
|
+
});
|
|
3692
|
+
} catch {}
|
|
3693
|
+
}
|
|
3462
3694
|
const pendingPurchaseStorageKey = `cohub-app-purchase:${app.id}`;
|
|
3463
3695
|
const purchaseInFlight = /* @__PURE__ */ new Map();
|
|
3464
3696
|
let activePurchase = null;
|
|
@@ -3501,13 +3733,14 @@ function createAppBridgeCore(config) {
|
|
|
3501
3733
|
* fresh dialog.
|
|
3502
3734
|
*/
|
|
3503
3735
|
async function authorize(scopes, spaceId, options) {
|
|
3504
|
-
const userToken = await getAccessToken();
|
|
3736
|
+
const userToken = await getAccessToken(options?.forceRefreshToken ? { forceRefresh: true } : void 0);
|
|
3505
3737
|
if (!userToken) {
|
|
3506
3738
|
await config.requestSignIn(typeof location !== "undefined" ? location.pathname : "/");
|
|
3507
3739
|
throw new Error("Sign in is required to authorize this app.");
|
|
3508
3740
|
}
|
|
3509
3741
|
const response = await fetch(`${apiOrigin}/api/apps/${app.id}/authorize`, {
|
|
3510
3742
|
method: "POST",
|
|
3743
|
+
signal: timeoutSignal(AUTHORIZE_TIMEOUT_MS),
|
|
3511
3744
|
headers: {
|
|
3512
3745
|
Authorization: `Bearer ${userToken}`,
|
|
3513
3746
|
"Content-Type": "application/json"
|
|
@@ -3519,7 +3752,7 @@ function createAppBridgeCore(config) {
|
|
|
3519
3752
|
})
|
|
3520
3753
|
});
|
|
3521
3754
|
const payload = await response.json().catch(() => null);
|
|
3522
|
-
if (!response.ok) throw new AppAuthorizationError(typeof payload?.message === "string" ? payload.message : "Authorization failed.", response.status);
|
|
3755
|
+
if (!response.ok) throw new AppAuthorizationError(typeof payload?.message === "string" ? payload.message : "Authorization failed.", response.status, typeof payload?.code === "string" ? payload.code : void 0);
|
|
3523
3756
|
const token = readTokenResponse(payload);
|
|
3524
3757
|
if (!token) throw new Error("Invalid app authorization response.");
|
|
3525
3758
|
const canonicalSpaceId = typeof payload?.grant?.spaceId === "string" && payload.grant.spaceId ? payload.grant.spaceId : spaceId ?? app.spaceId;
|
|
@@ -3538,6 +3771,64 @@ function createAppBridgeCore(config) {
|
|
|
3538
3771
|
};
|
|
3539
3772
|
}
|
|
3540
3773
|
/**
|
|
3774
|
+
* Silent renewal with a single stale-token retry: a 401 means the access
|
|
3775
|
+
* token expired, not that the grant was revoked, so refresh once before
|
|
3776
|
+
* letting the caller classify the failure.
|
|
3777
|
+
*/
|
|
3778
|
+
async function authorizeSilent(scopes, spaceId) {
|
|
3779
|
+
try {
|
|
3780
|
+
return await authorize(scopes, spaceId, { silent: true });
|
|
3781
|
+
} catch (error) {
|
|
3782
|
+
if (!isAuthFailure(error)) throw error;
|
|
3783
|
+
return authorize(scopes, spaceId, {
|
|
3784
|
+
silent: true,
|
|
3785
|
+
forceRefreshToken: true
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* Attempts silent renewal against each target in order. Returns true when the
|
|
3791
|
+
* request was answered (success or terminal error); false when every target
|
|
3792
|
+
* was a rejected grant, so the caller should fall back to consent.
|
|
3793
|
+
*/
|
|
3794
|
+
async function attemptSilentRenewal(reserved, viewerUuid, scopes, targets) {
|
|
3795
|
+
if (!viewerUuid || targets.length === 0) return false;
|
|
3796
|
+
for (const target of targets) try {
|
|
3797
|
+
const result = await authorizeSilent(scopes, target);
|
|
3798
|
+
if (state.pendingAuth !== reserved) return true;
|
|
3799
|
+
replyPendingAuth(reserved, authorizeResult(result.token, result.spaceId, null));
|
|
3800
|
+
resetDialogState();
|
|
3801
|
+
safeNotify();
|
|
3802
|
+
return true;
|
|
3803
|
+
} catch (error) {
|
|
3804
|
+
if (state.pendingAuth !== reserved) return true;
|
|
3805
|
+
if (isGrantRejection(error)) {
|
|
3806
|
+
clearGrantedAppScopes(viewerUuid, app.id, target);
|
|
3807
|
+
if (target === app.spaceId) clearGrantedAppScopes(viewerUuid, app.id);
|
|
3808
|
+
continue;
|
|
3809
|
+
}
|
|
3810
|
+
replyPendingAuth(reserved, {
|
|
3811
|
+
type: "cohub.app.error",
|
|
3812
|
+
message: error instanceof Error ? error.message : "Authorization failed."
|
|
3813
|
+
});
|
|
3814
|
+
resetDialogState();
|
|
3815
|
+
safeNotify();
|
|
3816
|
+
return true;
|
|
3817
|
+
}
|
|
3818
|
+
return false;
|
|
3819
|
+
}
|
|
3820
|
+
/** Normalizes a raw Space payload into a picker candidate. */
|
|
3821
|
+
function toSpaceOption(value) {
|
|
3822
|
+
if (typeof value.id !== "string" || !value.id) return null;
|
|
3823
|
+
const owner = value.ownerUserUuid ?? value.userUuid;
|
|
3824
|
+
return {
|
|
3825
|
+
id: value.id,
|
|
3826
|
+
name: typeof value.name === "string" && value.name ? value.name : null,
|
|
3827
|
+
...typeof owner === "string" ? { ownerUserUuid: owner } : {},
|
|
3828
|
+
...typeof value.isPinned === "boolean" ? { isPinned: value.isPinned } : {}
|
|
3829
|
+
};
|
|
3830
|
+
}
|
|
3831
|
+
/**
|
|
3541
3832
|
* Lists the viewer's spaces for the consent dialog picker. Fetched by the
|
|
3542
3833
|
* host with the viewer's own token — the app only ever learns the space
|
|
3543
3834
|
* the viewer picks, never the list.
|
|
@@ -3546,7 +3837,10 @@ function createAppBridgeCore(config) {
|
|
|
3546
3837
|
const request = async (forceRefresh = false) => {
|
|
3547
3838
|
const userToken = await getAccessToken({ forceRefresh });
|
|
3548
3839
|
if (!userToken) return null;
|
|
3549
|
-
return fetch(`${apiOrigin}/api/spaces`, {
|
|
3840
|
+
return fetch(`${apiOrigin}/api/spaces`, {
|
|
3841
|
+
headers: { Authorization: `Bearer ${userToken}` },
|
|
3842
|
+
signal: spaceListSignal()
|
|
3843
|
+
});
|
|
3550
3844
|
};
|
|
3551
3845
|
try {
|
|
3552
3846
|
let response = await request();
|
|
@@ -3555,14 +3849,33 @@ function createAppBridgeCore(config) {
|
|
|
3555
3849
|
const spaces = await response.json();
|
|
3556
3850
|
if (!Array.isArray(spaces)) return null;
|
|
3557
3851
|
return spaces.flatMap((space) => {
|
|
3558
|
-
|
|
3559
|
-
return [
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3852
|
+
const option = toSpaceOption(space);
|
|
3853
|
+
return option ? [option] : [];
|
|
3854
|
+
});
|
|
3855
|
+
} catch {
|
|
3856
|
+
return null;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
/**
|
|
3860
|
+
* Ensures the viewer has a Space to target when their accessible list is
|
|
3861
|
+
* empty. `GET /api/spaces/default` returns their home Space, creating it on
|
|
3862
|
+
* first use — the same path taken when they first enter Cohub.
|
|
3863
|
+
*/
|
|
3864
|
+
async function ensureDefaultSpace() {
|
|
3865
|
+
const request = async (forceRefresh = false) => {
|
|
3866
|
+
const userToken = await getAccessToken({ forceRefresh });
|
|
3867
|
+
if (!userToken) return null;
|
|
3868
|
+
return fetch(`${apiOrigin}/api/spaces/default`, {
|
|
3869
|
+
headers: { Authorization: `Bearer ${userToken}` },
|
|
3870
|
+
signal: spaceListSignal()
|
|
3565
3871
|
});
|
|
3872
|
+
};
|
|
3873
|
+
try {
|
|
3874
|
+
let response = await request();
|
|
3875
|
+
if (response?.status === 401) response = await request(true);
|
|
3876
|
+
if (!response?.ok) return null;
|
|
3877
|
+
const payload = await response.json();
|
|
3878
|
+
return payload?.space ? toSpaceOption(payload.space) : null;
|
|
3566
3879
|
} catch {
|
|
3567
3880
|
return null;
|
|
3568
3881
|
}
|
|
@@ -3581,6 +3894,27 @@ function createAppBridgeCore(config) {
|
|
|
3581
3894
|
} catch {}
|
|
3582
3895
|
}
|
|
3583
3896
|
/**
|
|
3897
|
+
* Resolves the default target for a Space-bound request to a Space the
|
|
3898
|
+
* viewer controls — preferring wherever the app was invoked, then the last
|
|
3899
|
+
* Space the viewer picked. Returns undefined when nothing is accessible.
|
|
3900
|
+
*/
|
|
3901
|
+
function resolveDefaultSpaceId(candidates) {
|
|
3902
|
+
if (!candidates?.length) return void 0;
|
|
3903
|
+
const accessible = (id) => id && candidates.some((space) => space.id === id) ? id : void 0;
|
|
3904
|
+
const invocation = activeInvocation !== void 0 ? activeInvocation : config.getInvocation?.() ?? config.invocation;
|
|
3905
|
+
const shell = config.getShell?.() ?? config.shell;
|
|
3906
|
+
return accessible(invocation?.spaceId) ?? accessible(shell?.space?.id) ?? accessible(readLastPickedSpace()) ?? candidates[0]?.id;
|
|
3907
|
+
}
|
|
3908
|
+
/**
|
|
3909
|
+
* The Space the app is currently in, from trusted host context only (no
|
|
3910
|
+
* network). Used to attempt silent renewal before loading the Space list.
|
|
3911
|
+
*/
|
|
3912
|
+
function resolveContextSpaceId() {
|
|
3913
|
+
const invocation = activeInvocation !== void 0 ? activeInvocation : config.getInvocation?.() ?? config.invocation;
|
|
3914
|
+
const shell = config.getShell?.() ?? config.shell;
|
|
3915
|
+
return invocation?.spaceId ?? shell?.space?.id ?? void 0;
|
|
3916
|
+
}
|
|
3917
|
+
/**
|
|
3584
3918
|
* Creates the Space with the viewer's own token — the same `POST /api/spaces`
|
|
3585
3919
|
* path as the web New Space flow and the CLI. The app never holds this token.
|
|
3586
3920
|
*/
|
|
@@ -3619,23 +3953,6 @@ function createAppBridgeCore(config) {
|
|
|
3619
3953
|
};
|
|
3620
3954
|
throw new Error(message);
|
|
3621
3955
|
}
|
|
3622
|
-
/**
|
|
3623
|
-
* Resolves the target space's name for the consent dialog. The host — not
|
|
3624
|
-
* the app — resolves it, so the dialog cannot be tricked into labeling a
|
|
3625
|
-
* grant with the wrong space.
|
|
3626
|
-
*/
|
|
3627
|
-
async function resolveSpaceName(spaceId) {
|
|
3628
|
-
const userToken = await getAccessToken();
|
|
3629
|
-
if (!userToken) return null;
|
|
3630
|
-
try {
|
|
3631
|
-
const response = await fetch(`${apiOrigin}/api/spaces/${encodeURIComponent(spaceId)}`, { headers: { Authorization: `Bearer ${userToken}` } });
|
|
3632
|
-
if (!response.ok) return null;
|
|
3633
|
-
const space = await response.json();
|
|
3634
|
-
return typeof space.name === "string" && space.name ? space.name : null;
|
|
3635
|
-
} catch {
|
|
3636
|
-
return null;
|
|
3637
|
-
}
|
|
3638
|
-
}
|
|
3639
3956
|
function writePendingPurchase(input) {
|
|
3640
3957
|
if (typeof sessionStorage === "undefined") return;
|
|
3641
3958
|
try {
|
|
@@ -3693,6 +4010,7 @@ function createAppBridgeCore(config) {
|
|
|
3693
4010
|
if (!data?.requestId) return;
|
|
3694
4011
|
const isLegacyWork = data.type?.startsWith("cohub.work.") === true;
|
|
3695
4012
|
if (isLegacyWork) legacyRequestIds.add(data.requestId);
|
|
4013
|
+
let reservedAuth = null;
|
|
3696
4014
|
try {
|
|
3697
4015
|
if (data.type === "cohub.app.context" || data.type === "cohub.work.context") {
|
|
3698
4016
|
const context = await getContext();
|
|
@@ -3820,20 +4138,24 @@ function createAppBridgeCore(config) {
|
|
|
3820
4138
|
}
|
|
3821
4139
|
if (createSpace) {
|
|
3822
4140
|
mintedSpace = null;
|
|
3823
|
-
|
|
4141
|
+
const pendingCreate = {
|
|
3824
4142
|
requestId: data.requestId,
|
|
3825
4143
|
scopes,
|
|
3826
4144
|
reason: sanitizeReason(data.reason),
|
|
3827
4145
|
homeSpaceName: app.spaceName ?? null,
|
|
3828
4146
|
createSpace
|
|
3829
4147
|
};
|
|
4148
|
+
state.pendingAuth = pendingCreate;
|
|
4149
|
+
reservedAuth = pendingCreate;
|
|
4150
|
+
state.selectedSpaceId = null;
|
|
4151
|
+
state.canChangeSpace = false;
|
|
3830
4152
|
state.authError = null;
|
|
3831
4153
|
state.authOpen = true;
|
|
3832
4154
|
notify();
|
|
3833
4155
|
return;
|
|
3834
4156
|
}
|
|
3835
4157
|
if (!selectSpace && !alwaysAsk && allowsOwnerAutoAuthorization() && await isCurrentViewerAppOwner()) try {
|
|
3836
|
-
const result = await
|
|
4158
|
+
const result = await authorizeSilent(scopes, spaceId);
|
|
3837
4159
|
replyForRequest(data.requestId, authorizeResult(result.token, result.spaceId, result.spaceId === app.spaceId ? app.spaceName ?? null : null), true);
|
|
3838
4160
|
return;
|
|
3839
4161
|
} catch (error) {
|
|
@@ -3845,45 +4167,82 @@ function createAppBridgeCore(config) {
|
|
|
3845
4167
|
return;
|
|
3846
4168
|
}
|
|
3847
4169
|
}
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
replyForRequest(data.requestId, {
|
|
3857
|
-
type: "cohub.app.error",
|
|
3858
|
-
message: error instanceof Error ? error.message : "Authorization failed."
|
|
3859
|
-
}, true);
|
|
4170
|
+
if (state.pendingAuth) {
|
|
4171
|
+
if (isSameConsent(state.pendingAuth, {
|
|
4172
|
+
scopes,
|
|
4173
|
+
spaceId,
|
|
4174
|
+
selectSpace,
|
|
4175
|
+
createSpace
|
|
4176
|
+
})) {
|
|
4177
|
+
state.pendingAuth.joinedRequestIds = [...state.pendingAuth.joinedRequestIds ?? [], data.requestId];
|
|
3860
4178
|
return;
|
|
3861
4179
|
}
|
|
3862
|
-
|
|
4180
|
+
dismissPendingAuth();
|
|
3863
4181
|
}
|
|
3864
|
-
const
|
|
3865
|
-
state.pendingAuth = {
|
|
4182
|
+
const reserved = {
|
|
3866
4183
|
requestId: data.requestId,
|
|
3867
4184
|
scopes,
|
|
3868
4185
|
reason: sanitizeReason(data.reason),
|
|
3869
4186
|
homeSpaceName: app.spaceName ?? null,
|
|
3870
|
-
...spaceId ? {
|
|
3871
|
-
|
|
3872
|
-
spaceName
|
|
3873
|
-
} : {},
|
|
3874
|
-
...selectSpace ? {
|
|
3875
|
-
selectSpace: true,
|
|
3876
|
-
spaces: spaces ?? null
|
|
3877
|
-
} : {}
|
|
4187
|
+
...spaceId ? { spaceId } : {},
|
|
4188
|
+
...selectSpace ? { selectSpace: true } : {}
|
|
3878
4189
|
};
|
|
4190
|
+
state.pendingAuth = reserved;
|
|
4191
|
+
reservedAuth = reserved;
|
|
4192
|
+
state.selectedSpaceId = null;
|
|
4193
|
+
state.canChangeSpace = false;
|
|
3879
4194
|
state.authError = null;
|
|
4195
|
+
const viewerUuid = await getViewerUuid();
|
|
4196
|
+
if (state.pendingAuth !== reserved) return;
|
|
4197
|
+
const spaceLevel = Boolean(spaceId) || selectSpace || scopes.some((scope) => !isUserLevelPermission(scope));
|
|
4198
|
+
const lastPicked = readLastPickedSpace();
|
|
4199
|
+
const contextSpaceId = resolveContextSpaceId();
|
|
4200
|
+
const preListTargets = !spaceLevel ? listGrantedAppSpacesForScopes(viewerUuid, app.id, scopes, app.spaceId) : spaceId === void 0 && !selectSpace && contextSpaceId && hasGrantedAppScopes(viewerUuid, app.id, scopes, contextSpaceId) ? [contextSpaceId] : [];
|
|
4201
|
+
if (!alwaysAsk && await attemptSilentRenewal(reserved, viewerUuid, scopes, preListTargets)) return;
|
|
4202
|
+
if (state.pendingAuth !== reserved) return;
|
|
4203
|
+
let spaces = spaceLevel ? await listViewerSpaces() : void 0;
|
|
4204
|
+
if (state.pendingAuth !== reserved) return;
|
|
4205
|
+
if (spaces && spaces.length === 0) {
|
|
4206
|
+
const fallback = await ensureDefaultSpace();
|
|
4207
|
+
if (state.pendingAuth !== reserved) return;
|
|
4208
|
+
if (fallback) spaces = [fallback];
|
|
4209
|
+
}
|
|
4210
|
+
const candidates = spaces ?? null;
|
|
4211
|
+
const explicitAccessible = Boolean(spaceId) && candidates?.some((space) => space.id === spaceId) === true;
|
|
4212
|
+
const defaultSpaceId = explicitAccessible ? spaceId : resolveDefaultSpaceId(candidates);
|
|
4213
|
+
const silentTargets = (!spaceLevel ? [] : [selectSpace ? lastPicked && candidates?.some((space) => space.id === lastPicked) ? lastPicked : null : explicitAccessible ? spaceId : spaceId === void 0 ? defaultSpaceId : null].filter((target) => Boolean(target) && hasGrantedAppScopes(viewerUuid, app.id, scopes, target ?? void 0))).slice(0, MAX_SILENT_SPACE_ATTEMPTS);
|
|
4214
|
+
if (!alwaysAsk && await attemptSilentRenewal(reserved, viewerUuid, scopes, silentTargets)) return;
|
|
4215
|
+
if (state.pendingAuth !== reserved) return;
|
|
4216
|
+
if (spaceId && candidates && !explicitAccessible) emitDiagnostic({
|
|
4217
|
+
code: "space_inaccessible",
|
|
4218
|
+
message: defaultSpaceId ? "The app requested a Space this viewer cannot access; using a Space the viewer controls instead." : "The app requested a Space this viewer cannot access, and no viewer-controlled Space was available.",
|
|
4219
|
+
detail: { requestedSpaceId: spaceId }
|
|
4220
|
+
});
|
|
4221
|
+
const spaceName = defaultSpaceId ? candidates?.find((space) => space.id === defaultSpaceId)?.name ?? null : null;
|
|
4222
|
+
Object.assign(reserved, {
|
|
4223
|
+
...explicitAccessible && spaceName ? { spaceName } : {},
|
|
4224
|
+
...spaces !== void 0 ? { spaces: spaces ?? null } : {},
|
|
4225
|
+
...defaultSpaceId ? { defaultSpaceId } : {}
|
|
4226
|
+
});
|
|
4227
|
+
state.selectedSpaceId = defaultSpaceId ?? null;
|
|
4228
|
+
state.canChangeSpace = !explicitAccessible && (candidates?.length ?? 0) > 1;
|
|
3880
4229
|
state.authOpen = true;
|
|
3881
4230
|
notify();
|
|
3882
4231
|
}
|
|
3883
4232
|
} catch (error) {
|
|
4233
|
+
const message = error instanceof Error ? error.message : "Request failed.";
|
|
4234
|
+
if (reservedAuth && state.pendingAuth === reservedAuth) {
|
|
4235
|
+
replyPendingAuth(reservedAuth, {
|
|
4236
|
+
type: "cohub.app.error",
|
|
4237
|
+
message
|
|
4238
|
+
});
|
|
4239
|
+
resetDialogState();
|
|
4240
|
+
safeNotify();
|
|
4241
|
+
return;
|
|
4242
|
+
}
|
|
3884
4243
|
replyForRequest(data.requestId, {
|
|
3885
4244
|
type: "cohub.app.error",
|
|
3886
|
-
message
|
|
4245
|
+
message
|
|
3887
4246
|
}, true);
|
|
3888
4247
|
}
|
|
3889
4248
|
}
|
|
@@ -3891,15 +4250,21 @@ function createAppBridgeCore(config) {
|
|
|
3891
4250
|
function replyPendingAuth(pending, payload) {
|
|
3892
4251
|
for (const requestId of [pending.requestId, ...pending.joinedRequestIds ?? []]) replyForRequest(requestId, payload, true);
|
|
3893
4252
|
}
|
|
4253
|
+
/** Closes the consent dialog and drops its transient selection state. */
|
|
4254
|
+
function resetDialogState() {
|
|
4255
|
+
state.pendingAuth = null;
|
|
4256
|
+
state.authOpen = false;
|
|
4257
|
+
state.authError = null;
|
|
4258
|
+
state.selectedSpaceId = null;
|
|
4259
|
+
state.canChangeSpace = false;
|
|
4260
|
+
}
|
|
3894
4261
|
function dismissPendingAuth() {
|
|
3895
4262
|
if (!state.pendingAuth) return;
|
|
3896
4263
|
replyPendingAuth(state.pendingAuth, {
|
|
3897
4264
|
type: "cohub.app.authorize.result",
|
|
3898
4265
|
token: null
|
|
3899
4266
|
});
|
|
3900
|
-
|
|
3901
|
-
state.authOpen = false;
|
|
3902
|
-
state.authError = null;
|
|
4267
|
+
resetDialogState();
|
|
3903
4268
|
mintedSpace = null;
|
|
3904
4269
|
notify();
|
|
3905
4270
|
}
|
|
@@ -3910,6 +4275,17 @@ function createAppBridgeCore(config) {
|
|
|
3910
4275
|
state.authSaving = false;
|
|
3911
4276
|
notify();
|
|
3912
4277
|
}
|
|
4278
|
+
/**
|
|
4279
|
+
* Only a Space the host actually loaded may become the target, so a UI bug
|
|
4280
|
+
* cannot mint a grant against an arbitrary id.
|
|
4281
|
+
*/
|
|
4282
|
+
function setSelectedSpace(spaceId) {
|
|
4283
|
+
if (!state.pendingAuth || state.authSaving || !state.canChangeSpace) return;
|
|
4284
|
+
if (!state.pendingAuth.spaces?.some((space) => space.id === spaceId)) return;
|
|
4285
|
+
if (state.selectedSpaceId === spaceId) return;
|
|
4286
|
+
state.selectedSpaceId = spaceId;
|
|
4287
|
+
notify();
|
|
4288
|
+
}
|
|
3913
4289
|
async function executePurchase(purchase) {
|
|
3914
4290
|
const checkout = await createPurchase(purchase.productKey, purchase.purchaseAttemptId);
|
|
3915
4291
|
if (checkout && typeof checkout === "object") {
|
|
@@ -3946,38 +4322,50 @@ function createAppBridgeCore(config) {
|
|
|
3946
4322
|
async function confirmAuth(pickedSpaceId) {
|
|
3947
4323
|
if (!state.pendingAuth || state.authSaving) return;
|
|
3948
4324
|
const pending = state.pendingAuth;
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
4325
|
+
let requestedSpaceId = (state.canChangeSpace ? pickedSpaceId : void 0) ?? state.selectedSpaceId ?? void 0;
|
|
4326
|
+
if (pending.spaces !== void 0) {
|
|
4327
|
+
const candidates = pending.spaces;
|
|
4328
|
+
if (!(requestedSpaceId !== void 0 && candidates?.some((space) => space.id === requestedSpaceId) === true)) {
|
|
4329
|
+
state.authError = !candidates || candidates.length === 0 ? "Couldn't load your Spaces. Please try again." : "Choose a Space to continue.";
|
|
4330
|
+
notify();
|
|
4331
|
+
return;
|
|
4332
|
+
}
|
|
3953
4333
|
}
|
|
3954
4334
|
state.authError = null;
|
|
3955
4335
|
state.authSaving = true;
|
|
3956
4336
|
notify();
|
|
3957
4337
|
try {
|
|
3958
|
-
let requestedSpaceId = pending.selectSpace ? pickedSpaceId : pending.spaceId;
|
|
3959
|
-
let spaceName = pending.selectSpace ? pending.spaces?.find((space) => space.id === requestedSpaceId)?.name ?? null : pending.spaceName ?? app.spaceName ?? null;
|
|
3960
4338
|
if (pending.createSpace) {
|
|
3961
4339
|
mintedSpace ??= await createViewerSpace(pending.createSpace);
|
|
3962
4340
|
if (!mintedSpace.provisioned) {
|
|
3963
4341
|
replyPendingAuth(pending, authorizeResult(null, mintedSpace.id, mintedSpace.name ?? pending.createSpace.name));
|
|
3964
|
-
|
|
3965
|
-
state.pendingAuth = null;
|
|
4342
|
+
resetDialogState();
|
|
3966
4343
|
mintedSpace = null;
|
|
3967
4344
|
return;
|
|
3968
4345
|
}
|
|
3969
4346
|
requestedSpaceId = mintedSpace.id;
|
|
3970
|
-
spaceName = mintedSpace.name ?? pending.createSpace.name;
|
|
3971
4347
|
}
|
|
3972
4348
|
const result = await authorize(pending.scopes, requestedSpaceId);
|
|
3973
4349
|
setGrantedAppScopes(await getViewerUuid(), app.id, result.scopes, result.spaceId);
|
|
3974
|
-
if (pending.selectSpace || pending.createSpace) writeLastPickedSpace(result.spaceId);
|
|
4350
|
+
if (pending.selectSpace || pending.createSpace || pickedSpaceId && pickedSpaceId !== pending.spaceId) writeLastPickedSpace(result.spaceId);
|
|
4351
|
+
let spaceName = null;
|
|
4352
|
+
if (pending.createSpace) spaceName = mintedSpace?.name ?? pending.createSpace.name;
|
|
4353
|
+
else {
|
|
4354
|
+
const listed = pending.spaces?.find((space) => space.id === result.spaceId)?.name;
|
|
4355
|
+
if (listed) spaceName = listed;
|
|
4356
|
+
else if (result.spaceId === pending.spaceId) spaceName = pending.spaceName ?? null;
|
|
4357
|
+
else if (result.spaceId === app.spaceId) spaceName = app.spaceName ?? null;
|
|
4358
|
+
}
|
|
3975
4359
|
replyPendingAuth(pending, authorizeResult(result.token, result.spaceId, spaceName));
|
|
3976
|
-
|
|
3977
|
-
state.pendingAuth = null;
|
|
4360
|
+
resetDialogState();
|
|
3978
4361
|
mintedSpace = null;
|
|
3979
4362
|
} catch (error) {
|
|
3980
|
-
|
|
4363
|
+
if (error instanceof AppAuthorizationError) emitDiagnostic({
|
|
4364
|
+
code: error.code ?? "authorize_failed",
|
|
4365
|
+
message: error.message,
|
|
4366
|
+
detail: { scopes: pending.scopes }
|
|
4367
|
+
});
|
|
4368
|
+
state.authError = viewerOpaqueAuthMessage(error) ?? (error instanceof Error ? error.message : "Authorization failed.");
|
|
3981
4369
|
} finally {
|
|
3982
4370
|
state.authSaving = false;
|
|
3983
4371
|
notify();
|
|
@@ -3987,6 +4375,7 @@ function createAppBridgeCore(config) {
|
|
|
3987
4375
|
getState: () => ({ ...state }),
|
|
3988
4376
|
handleMessage,
|
|
3989
4377
|
notifyContextChanged,
|
|
4378
|
+
setSelectedSpace,
|
|
3990
4379
|
confirmAuth,
|
|
3991
4380
|
cancelAuth
|
|
3992
4381
|
};
|
|
@@ -4237,4 +4626,4 @@ function createBoardExtensionRegistry() {
|
|
|
4237
4626
|
}
|
|
4238
4627
|
const BOARD_CHANNELS = BOARD_ANIMATION_CHANNELS;
|
|
4239
4628
|
//#endregion
|
|
4240
|
-
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, BoardAnimationSpecSchema, BoardAuthoringItemSchema, BoardClient, BoardCompositionInputSchema, BoardCompositionSchema, BoardDealParamsSchema, 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, SpaceWebhooksApi, 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, resolveOpenSurface, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebsocketUrl, sampleCompositionTracks, sampleEasing, sampleTrack, sanitizeAccessToken, scopeListHasPermission, setGrantedAppScopes, track };
|
|
4629
|
+
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, BoardAnimationSpecSchema, BoardAuthoringItemSchema, BoardClient, BoardCompositionInputSchema, BoardCompositionSchema, BoardDealParamsSchema, 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, SpaceWebhooksApi, USER_LEVEL_PERMISSIONS, 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, filterSpacePickerItems, findGenerationModelPolicy, formatAppRef, formatWorkRef, getAllowedGenerationModelIds, getCohubContext, hasGrantedAppScopes, hasRequestSourceIdentity, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUserLevelPermission, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, normalizeBaseUrl, normalizeGenerationPolicy, normalizeRequestSource, normalizeSpacePickerQuery, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, orderSpacePickerItems, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardCompositionInput, parseBoardEffectInput, parseBoardPlaybackPolicy, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseWorkRef, proceduralClip, readRequestSourceFromEnv, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveOpenSurface, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebsocketUrl, sampleCompositionTracks, sampleEasing, sampleTrack, sanitizeAccessToken, scopeListHasPermission, selectSpacePickerItems, setGrantedAppScopes, track };
|