pi-studio 0.9.39 → 0.9.41
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/CHANGELOG.md +12 -0
- package/client/studio-client.js +300 -249
- package/client/studio-navigation-helpers.js +530 -0
- package/index.ts +85 -27
- package/package.json +2 -1
- package/shared/studio-repl-tmux.js +28 -0
- package/shared/studio-tab-launcher.js +86 -0
package/index.ts
CHANGED
|
@@ -29,7 +29,14 @@ import {
|
|
|
29
29
|
} from "./shared/studio-markdown-latex-literals.js";
|
|
30
30
|
import { escapeStudioPdfLatexTextFragment } from "./shared/studio-pdf-escape.js";
|
|
31
31
|
import { resolveStudioPdfResourceFile } from "./shared/studio-pdf-resource.js";
|
|
32
|
+
import { buildStudioReplTmuxStartArgs } from "./shared/studio-repl-tmux.js";
|
|
32
33
|
import { buildStudioForwardingHint, buildStudioSshTunnelHint, isStudioSshSession as isSshSession } from "./shared/studio-ssh-hint.js";
|
|
34
|
+
import {
|
|
35
|
+
buildStudioPendingPage,
|
|
36
|
+
buildStudioPendingSecurityHeaders,
|
|
37
|
+
isValidStudioLaunchId,
|
|
38
|
+
normalizeStudioPendingKind,
|
|
39
|
+
} from "./shared/studio-tab-launcher.js";
|
|
33
40
|
import { renderStudioAnnotationInlineHtml } from "./shared/studio-annotation-render.js";
|
|
34
41
|
import {
|
|
35
42
|
buildStudioMermaidCliIconArgs,
|
|
@@ -89,6 +96,7 @@ interface StudioQuartoPreviewState {
|
|
|
89
96
|
const STUDIO_CSS_URL = new URL("./client/studio.css", import.meta.url);
|
|
90
97
|
const STUDIO_ANNOTATION_HELPERS_URL = new URL("./client/studio-annotation-helpers.js", import.meta.url);
|
|
91
98
|
const STUDIO_MERMAID_HELPERS_URL = new URL("./client/studio-mermaid-helpers.js", import.meta.url);
|
|
99
|
+
const STUDIO_NAVIGATION_HELPERS_URL = new URL("./client/studio-navigation-helpers.js", import.meta.url);
|
|
92
100
|
const STUDIO_CLIENT_URL = new URL("./client/studio-client.js", import.meta.url);
|
|
93
101
|
|
|
94
102
|
interface StudioServerState {
|
|
@@ -7258,6 +7266,17 @@ function respondText(res: ServerResponse, status: number, text: string): void {
|
|
|
7258
7266
|
res.end(text);
|
|
7259
7267
|
}
|
|
7260
7268
|
|
|
7269
|
+
function respondStudioPendingError(res: ServerResponse, status: number, text: string, allow?: string): void {
|
|
7270
|
+
const nonce = randomUUID().replace(/-/g, "");
|
|
7271
|
+
const headers = {
|
|
7272
|
+
...buildStudioPendingSecurityHeaders(nonce),
|
|
7273
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
7274
|
+
...(allow ? { Allow: allow } : {}),
|
|
7275
|
+
};
|
|
7276
|
+
res.writeHead(status, headers);
|
|
7277
|
+
res.end(text);
|
|
7278
|
+
}
|
|
7279
|
+
|
|
7261
7280
|
function respondPdfFile(req: IncomingMessage, res: ServerResponse, filePath: string): void {
|
|
7262
7281
|
const method = (req.method ?? "GET").toUpperCase();
|
|
7263
7282
|
if (method !== "GET" && method !== "HEAD") {
|
|
@@ -7393,12 +7412,9 @@ async function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResp
|
|
|
7393
7412
|
}
|
|
7394
7413
|
const document = buildStudioLocalResourcePreviewDocument(resource);
|
|
7395
7414
|
const docId = storeTransientStudioDocument(document);
|
|
7396
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
7397
|
-
const parsedUrl = new URL(url);
|
|
7398
7415
|
respondJson(res, 200, {
|
|
7399
7416
|
...basePayload,
|
|
7400
|
-
|
|
7401
|
-
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
7417
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true }),
|
|
7402
7418
|
});
|
|
7403
7419
|
return;
|
|
7404
7420
|
}
|
|
@@ -7458,13 +7474,10 @@ async function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResp
|
|
|
7458
7474
|
}
|
|
7459
7475
|
|
|
7460
7476
|
const docId = storeTransientStudioDocument(document);
|
|
7461
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
7462
|
-
const parsedUrl = new URL(url);
|
|
7463
7477
|
respondJson(res, 200, {
|
|
7464
7478
|
...basePayload,
|
|
7465
7479
|
converted,
|
|
7466
|
-
|
|
7467
|
-
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
7480
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true }),
|
|
7468
7481
|
});
|
|
7469
7482
|
}
|
|
7470
7483
|
|
|
@@ -9626,7 +9639,7 @@ function startStudioReplSession(runtime: StudioReplRuntime, cwd: string, options
|
|
|
9626
9639
|
};
|
|
9627
9640
|
}
|
|
9628
9641
|
const command = getStudioReplRuntimeCommand(runtime, commandOverride);
|
|
9629
|
-
const result = runStudioTmux(
|
|
9642
|
+
const result = runStudioTmux(buildStudioReplTmuxStartArgs(sessionName, cwd || process.cwd(), command), { timeout: 5_000 });
|
|
9630
9643
|
if (!result.ok) return { ok: false, message: result.message || `Failed to start ${STUDIO_REPL_RUNTIME_LABELS[runtime]} REPL.` };
|
|
9631
9644
|
return {
|
|
9632
9645
|
ok: true,
|
|
@@ -10077,8 +10090,7 @@ function readTransientStudioDocument(id: string): InitialStudioDocument | null {
|
|
|
10077
10090
|
return entry ? { ...entry.document } : null;
|
|
10078
10091
|
}
|
|
10079
10092
|
|
|
10080
|
-
function
|
|
10081
|
-
port: number,
|
|
10093
|
+
function buildStudioRelativeUrl(
|
|
10082
10094
|
token: string,
|
|
10083
10095
|
mode: StudioUiMode = "full",
|
|
10084
10096
|
doc?: InitialStudioDocument | null,
|
|
@@ -10094,7 +10106,18 @@ function buildStudioUrl(
|
|
|
10094
10106
|
if (doc?.draftId) params.set("draftId", doc.draftId);
|
|
10095
10107
|
if (doc?.resourceDir) params.set("resourceDir", doc.resourceDir);
|
|
10096
10108
|
if (options?.skipWorkspaceRestore) params.set("skipWorkspaceRestore", "1");
|
|
10097
|
-
return
|
|
10109
|
+
return `/?${params.toString()}`;
|
|
10110
|
+
}
|
|
10111
|
+
|
|
10112
|
+
function buildStudioUrl(
|
|
10113
|
+
port: number,
|
|
10114
|
+
token: string,
|
|
10115
|
+
mode: StudioUiMode = "full",
|
|
10116
|
+
doc?: InitialStudioDocument | null,
|
|
10117
|
+
docId?: string,
|
|
10118
|
+
options?: { skipWorkspaceRestore?: boolean },
|
|
10119
|
+
): string {
|
|
10120
|
+
return `http://127.0.0.1:${port}${buildStudioRelativeUrl(token, mode, doc, docId, options)}`;
|
|
10098
10121
|
}
|
|
10099
10122
|
|
|
10100
10123
|
interface StudioLaunchFlags {
|
|
@@ -10508,6 +10531,7 @@ function buildStudioHtml(
|
|
|
10508
10531
|
const stylesheetHref = `/studio.css?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10509
10532
|
const annotationHelpersScriptHref = `/studio-annotation-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10510
10533
|
const mermaidHelpersScriptHref = `/studio-mermaid-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10534
|
+
const navigationHelpersScriptHref = `/studio-navigation-helpers.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10511
10535
|
const clientScriptHref = `/studio-client.js?token=${encodeURIComponent(studioToken ?? "")}`;
|
|
10512
10536
|
const faviconHref = buildStudioFaviconDataUri(style);
|
|
10513
10537
|
const bootConfigJson = JSON.stringify({ mermaidConfig }).replace(/</g, "\\u003c");
|
|
@@ -10926,6 +10950,7 @@ ${cssVarsBlock}
|
|
|
10926
10950
|
</script>
|
|
10927
10951
|
<script src="${annotationHelpersScriptHref}"></script>
|
|
10928
10952
|
<script src="${mermaidHelpersScriptHref}"></script>
|
|
10953
|
+
<script src="${navigationHelpersScriptHref}"></script>
|
|
10929
10954
|
<script src="${clientScriptHref}"></script>
|
|
10930
10955
|
</body>
|
|
10931
10956
|
</html>`;
|
|
@@ -12851,13 +12876,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
12851
12876
|
resourceDir,
|
|
12852
12877
|
};
|
|
12853
12878
|
const docId = storeTransientStudioDocument(document);
|
|
12854
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
12855
|
-
const parsedUrl = new URL(url);
|
|
12856
12879
|
sendToClient(client, {
|
|
12857
12880
|
type: "editor_only_ready",
|
|
12858
12881
|
requestId: msg.requestId,
|
|
12859
|
-
|
|
12860
|
-
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
12882
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true }),
|
|
12861
12883
|
message: hasContent
|
|
12862
12884
|
? "Editor tab is ready with a detached copy of the current editor text."
|
|
12863
12885
|
: "Blank editor tab is ready.",
|
|
@@ -14237,8 +14259,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
14237
14259
|
resourceDir: dirname(exportedPath),
|
|
14238
14260
|
};
|
|
14239
14261
|
const docId = storeTransientStudioDocument(document);
|
|
14240
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
14241
|
-
const parsedUrl = new URL(url);
|
|
14242
14262
|
respondJson(res, 200, {
|
|
14243
14263
|
ok: true,
|
|
14244
14264
|
filename,
|
|
@@ -14246,8 +14266,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14246
14266
|
writeError: writeResult.error,
|
|
14247
14267
|
warning: warning ?? null,
|
|
14248
14268
|
openedStudio: true,
|
|
14249
|
-
|
|
14250
|
-
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
14269
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true }),
|
|
14251
14270
|
downloadUrl: `/export-pdf?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
14252
14271
|
});
|
|
14253
14272
|
return;
|
|
@@ -14384,8 +14403,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
14384
14403
|
draftId: exportedPath ? undefined : createStudioDraftId(),
|
|
14385
14404
|
};
|
|
14386
14405
|
const docId = storeTransientStudioDocument(document);
|
|
14387
|
-
const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
|
|
14388
|
-
const parsedUrl = new URL(url);
|
|
14389
14406
|
respondJson(res, 200, {
|
|
14390
14407
|
ok: true,
|
|
14391
14408
|
filename,
|
|
@@ -14393,8 +14410,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14393
14410
|
writeError: writeResult.error,
|
|
14394
14411
|
warning: warning ?? null,
|
|
14395
14412
|
openedStudio: true,
|
|
14396
|
-
|
|
14397
|
-
relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
14413
|
+
relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true }),
|
|
14398
14414
|
downloadUrl: `/export-html?token=${encodeURIComponent(token)}&id=${encodeURIComponent(exportId)}`,
|
|
14399
14415
|
});
|
|
14400
14416
|
return;
|
|
@@ -14453,6 +14469,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
14453
14469
|
return;
|
|
14454
14470
|
}
|
|
14455
14471
|
|
|
14472
|
+
if (requestUrl.pathname === "/studio-open-pending") {
|
|
14473
|
+
const tokens = requestUrl.searchParams.getAll("token");
|
|
14474
|
+
if (tokens.length !== 1 || tokens[0] !== serverState.token) {
|
|
14475
|
+
respondStudioPendingError(res, 403, "Invalid or expired studio token. Re-run /studio.");
|
|
14476
|
+
return;
|
|
14477
|
+
}
|
|
14478
|
+
|
|
14479
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
14480
|
+
if (method !== "GET") {
|
|
14481
|
+
respondStudioPendingError(res, 405, "Method not allowed. Use GET.", "GET");
|
|
14482
|
+
return;
|
|
14483
|
+
}
|
|
14484
|
+
|
|
14485
|
+
const allowedKeys = new Set(["token", "launchId", "kind"]);
|
|
14486
|
+
if (Array.from(requestUrl.searchParams.keys()).some((key) => !allowedKeys.has(key))) {
|
|
14487
|
+
respondStudioPendingError(res, 400, "Unsupported Studio pending-page parameter.");
|
|
14488
|
+
return;
|
|
14489
|
+
}
|
|
14490
|
+
const launchIds = requestUrl.searchParams.getAll("launchId");
|
|
14491
|
+
const kinds = requestUrl.searchParams.getAll("kind");
|
|
14492
|
+
const launchId = launchIds.length === 1 ? launchIds[0] : "";
|
|
14493
|
+
const kind = kinds.length === 1 ? normalizeStudioPendingKind(kinds[0]) : null;
|
|
14494
|
+
if (!isValidStudioLaunchId(launchId) || !kind) {
|
|
14495
|
+
respondStudioPendingError(res, 400, "Invalid Studio pending-page request.");
|
|
14496
|
+
return;
|
|
14497
|
+
}
|
|
14498
|
+
|
|
14499
|
+
const nonce = randomUUID().replace(/-/g, "");
|
|
14500
|
+
res.writeHead(200, buildStudioPendingSecurityHeaders(nonce));
|
|
14501
|
+
res.end(buildStudioPendingPage({ token: serverState.token, launchId, kind, nonce }));
|
|
14502
|
+
return;
|
|
14503
|
+
}
|
|
14504
|
+
|
|
14456
14505
|
if (requestUrl.pathname === "/studio.css") {
|
|
14457
14506
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
14458
14507
|
if (token !== serverState.token) {
|
|
@@ -14482,7 +14531,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
14482
14531
|
return;
|
|
14483
14532
|
}
|
|
14484
14533
|
|
|
14485
|
-
if (
|
|
14534
|
+
if (
|
|
14535
|
+
requestUrl.pathname === "/studio-annotation-helpers.js"
|
|
14536
|
+
|| requestUrl.pathname === "/studio-mermaid-helpers.js"
|
|
14537
|
+
|| requestUrl.pathname === "/studio-navigation-helpers.js"
|
|
14538
|
+
|| requestUrl.pathname === "/studio-client.js"
|
|
14539
|
+
) {
|
|
14486
14540
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
14487
14541
|
if (token !== serverState.token) {
|
|
14488
14542
|
respondText(res, 403, "Invalid or expired studio token. Re-run /studio.");
|
|
@@ -14500,12 +14554,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
14500
14554
|
? STUDIO_ANNOTATION_HELPERS_URL
|
|
14501
14555
|
: requestUrl.pathname === "/studio-mermaid-helpers.js"
|
|
14502
14556
|
? STUDIO_MERMAID_HELPERS_URL
|
|
14503
|
-
:
|
|
14557
|
+
: requestUrl.pathname === "/studio-navigation-helpers.js"
|
|
14558
|
+
? STUDIO_NAVIGATION_HELPERS_URL
|
|
14559
|
+
: STUDIO_CLIENT_URL;
|
|
14504
14560
|
const targetLabel = requestUrl.pathname === "/studio-annotation-helpers.js"
|
|
14505
14561
|
? "studio annotation helper script"
|
|
14506
14562
|
: requestUrl.pathname === "/studio-mermaid-helpers.js"
|
|
14507
14563
|
? "studio Mermaid helper script"
|
|
14508
|
-
:
|
|
14564
|
+
: requestUrl.pathname === "/studio-navigation-helpers.js"
|
|
14565
|
+
? "studio navigation helper script"
|
|
14566
|
+
: "studio client script";
|
|
14509
14567
|
|
|
14510
14568
|
try {
|
|
14511
14569
|
const clientScript = readFileSync(targetUrl, "utf-8");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.41",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
32
|
"test": "node --test",
|
|
33
|
+
"test:browser": "node --test test/studio-tab-launcher-browser.test.js",
|
|
33
34
|
"typecheck": "tsc --noEmit"
|
|
34
35
|
},
|
|
35
36
|
"pi": {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const STUDIO_REPL_PROMPT_COMMAND_ENVIRONMENT = "PROMPT_COMMAND=";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build the tmux arguments for a Studio-owned REPL session.
|
|
5
|
+
*
|
|
6
|
+
* PROMPT_COMMAND contains shell-local function names on some terminals (notably
|
|
7
|
+
* cmux/Ghostty with macOS Bash 3.2). The variable may be exported to Pi while
|
|
8
|
+
* the corresponding functions cannot be inherited by a child shell, so start
|
|
9
|
+
* each detached REPL with a clean prompt-command environment.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} sessionName
|
|
12
|
+
* @param {string} cwd
|
|
13
|
+
* @param {string} command
|
|
14
|
+
* @returns {string[]}
|
|
15
|
+
*/
|
|
16
|
+
export function buildStudioReplTmuxStartArgs(sessionName, cwd, command) {
|
|
17
|
+
return [
|
|
18
|
+
"new-session",
|
|
19
|
+
"-d",
|
|
20
|
+
"-s",
|
|
21
|
+
sessionName,
|
|
22
|
+
"-c",
|
|
23
|
+
cwd,
|
|
24
|
+
"-e",
|
|
25
|
+
STUDIO_REPL_PROMPT_COMMAND_ENVIRONMENT,
|
|
26
|
+
command,
|
|
27
|
+
];
|
|
28
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const STUDIO_PENDING_KINDS = Object.freeze(["document", "preview", "export"]);
|
|
2
|
+
const STUDIO_LAUNCH_ID_PATTERN = /^[a-zA-Z0-9_-]{20,128}$/;
|
|
3
|
+
const STUDIO_CSP_NONCE_PATTERN = /^[a-zA-Z0-9_-]{16,128}$/;
|
|
4
|
+
|
|
5
|
+
function escapeHtmlAttribute(value) {
|
|
6
|
+
return String(value ?? "")
|
|
7
|
+
.replace(/&/g, "&")
|
|
8
|
+
.replace(/"/g, """)
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeStudioPendingKind(value) {
|
|
14
|
+
return STUDIO_PENDING_KINDS.includes(value) ? value : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isValidStudioLaunchId(value) {
|
|
18
|
+
return typeof value === "string" && STUDIO_LAUNCH_ID_PATTERN.test(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildStudioPendingSecurityHeaders(nonce) {
|
|
22
|
+
if (typeof nonce !== "string" || !STUDIO_CSP_NONCE_PATTERN.test(nonce)) {
|
|
23
|
+
throw new Error("Invalid Studio pending-page CSP nonce.");
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
27
|
+
"Cache-Control": "no-store",
|
|
28
|
+
"X-Content-Type-Options": "nosniff",
|
|
29
|
+
"Referrer-Policy": "no-referrer",
|
|
30
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
31
|
+
"Cross-Origin-Resource-Policy": "same-origin",
|
|
32
|
+
"Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=()",
|
|
33
|
+
"Content-Security-Policy": [
|
|
34
|
+
"default-src 'none'",
|
|
35
|
+
`script-src 'nonce-${nonce}'`,
|
|
36
|
+
`style-src 'nonce-${nonce}'`,
|
|
37
|
+
"base-uri 'none'",
|
|
38
|
+
"form-action 'none'",
|
|
39
|
+
"object-src 'none'",
|
|
40
|
+
"frame-ancestors 'none'",
|
|
41
|
+
].join("; "),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildStudioPendingPage(options) {
|
|
46
|
+
const config = options && typeof options === "object" ? options : {};
|
|
47
|
+
const token = typeof config.token === "string" ? config.token : "";
|
|
48
|
+
const launchId = typeof config.launchId === "string" ? config.launchId : "";
|
|
49
|
+
const kind = normalizeStudioPendingKind(config.kind);
|
|
50
|
+
const nonce = typeof config.nonce === "string" ? config.nonce : "";
|
|
51
|
+
if (!token) throw new Error("Studio pending page requires a token.");
|
|
52
|
+
if (!isValidStudioLaunchId(launchId)) throw new Error("Invalid Studio launch ID.");
|
|
53
|
+
if (!kind) throw new Error("Invalid Studio pending-page kind.");
|
|
54
|
+
buildStudioPendingSecurityHeaders(nonce);
|
|
55
|
+
|
|
56
|
+
const kindLabel = kind === "document" ? "document" : (kind === "export" ? "export" : "preview");
|
|
57
|
+
const helperQuery = new URLSearchParams({ token }).toString();
|
|
58
|
+
return `<!doctype html>
|
|
59
|
+
<html lang="en">
|
|
60
|
+
<head>
|
|
61
|
+
<meta charset="utf-8" />
|
|
62
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
63
|
+
<title>Preparing Studio tab…</title>
|
|
64
|
+
<style nonce="${escapeHtmlAttribute(nonce)}">
|
|
65
|
+
:root { color-scheme: light dark; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
66
|
+
body { min-height: 100vh; margin: 0; display: grid; place-items: center; background: Canvas; color: CanvasText; }
|
|
67
|
+
main { width: min(34rem, calc(100vw - 3rem)); padding: 2rem; border: 1px solid color-mix(in srgb, CanvasText 18%, transparent); border-radius: 12px; }
|
|
68
|
+
h1 { margin: 0 0 0.65rem; font-size: 1.15rem; }
|
|
69
|
+
p { margin: 0; line-height: 1.5; opacity: 0.8; overflow-wrap: anywhere; }
|
|
70
|
+
button { margin-top: 1.25rem; padding: 0.45rem 0.8rem; font: inherit; }
|
|
71
|
+
[hidden] { display: none !important; }
|
|
72
|
+
</style>
|
|
73
|
+
</head>
|
|
74
|
+
<body data-studio-pending-launch="1" data-launch-id="${escapeHtmlAttribute(launchId)}" data-launch-kind="${escapeHtmlAttribute(kind)}" data-studio-token="${escapeHtmlAttribute(token)}">
|
|
75
|
+
<main aria-live="polite" aria-atomic="true">
|
|
76
|
+
<h1 id="pendingTitle">Preparing Studio ${kindLabel}…</h1>
|
|
77
|
+
<p id="pendingDetail">Waiting for the originating Studio page.</p>
|
|
78
|
+
<button id="pendingCloseBtn" type="button" hidden>Close tab</button>
|
|
79
|
+
</main>
|
|
80
|
+
<noscript>This Studio tab requires JavaScript. You can close it and return to the originating Studio page.</noscript>
|
|
81
|
+
<script nonce="${escapeHtmlAttribute(nonce)}" src="/studio-navigation-helpers.js?${escapeHtmlAttribute(helperQuery)}"></script>
|
|
82
|
+
</body>
|
|
83
|
+
</html>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { STUDIO_PENDING_KINDS };
|