@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/board/core/file-preview.d.ts +18 -139
- package/dist/board/core/file-preview.js +23 -200
- package/dist/board/core/file-snapshot.d.ts +106 -0
- package/dist/board/core/file-snapshot.js +503 -0
- package/dist/board/index.d.ts +3 -2
- package/dist/board/index.js +3 -2
- package/dist/board/render/renderers/file-card-renderer.js +92 -18
- package/dist/chunks/http.d.ts +34 -3
- package/dist/index.d.ts +57 -1
- package/dist/index.js +349 -20
- package/docs/app-runtime-guide.md +25 -3
- package/package.json +1 -1
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1176
|
-
const isSurfaceEnvelope = (value) => isRecord$
|
|
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$
|
|
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$
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|