veryfront 0.1.31 → 0.1.32

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/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -8,6 +8,10 @@ export interface StudioScriptOptions {
8
8
  nonce?: string;
9
9
  /** Hash of source code for sync detection with Navigator tree */
10
10
  sourceHash?: string;
11
+ /** WebSocket URL for direct Yjs connection from the bridge */
12
+ wsUrl?: string;
13
+ /** Yjs document GUID for the bridge to join the same room */
14
+ yjsGuid?: string;
11
15
  }
12
16
  export declare function getStudioScripts(options: StudioScriptOptions): string;
13
17
  //# sourceMappingURL=dev-scripts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dev-scripts.d.ts","sourceRoot":"","sources":["../../../src/src/html/dev-scripts.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CA+BnD;AAMD,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAMvE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAOnE;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAerE"}
1
+ {"version":3,"file":"dev-scripts.d.ts","sourceRoot":"","sources":["../../../src/src/html/dev-scripts.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CA+BnD;AAMD,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAMvE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAOnE;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAiBrE"}
@@ -51,6 +51,8 @@ export function getStudioScripts(options) {
51
51
  projectId: options.projectId,
52
52
  pageId: options.pageId,
53
53
  ...(options.pagePath ? { pagePath: options.pagePath } : {}),
54
+ ...(options.wsUrl ? { wsUrl: options.wsUrl } : {}),
55
+ ...(options.yjsGuid ? { yjsGuid: options.yjsGuid } : {}),
54
56
  }).toString();
55
57
  const sourceHashScript = options.sourceHash
56
58
  ? `<script${nonceAttr}>window.__VERYFRONT_SOURCE_HASH__="${options.sourceHash}";</script>\n `
@@ -15,6 +15,10 @@ export interface InjectHTMLContentOptions {
15
15
  pageId?: string;
16
16
  /** CSP nonce */
17
17
  nonce?: string;
18
+ /** WebSocket URL for direct Yjs connection from the bridge */
19
+ wsUrl?: string;
20
+ /** Yjs document GUID for the bridge to join the same room */
21
+ yjsGuid?: string;
18
22
  }
19
23
  export declare function injectHTMLContent(template: string, content: string, metadata: HTMLMetadata, options: InjectHTMLContentOptions): string;
20
24
  //# sourceMappingURL=html-injection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"html-injection.d.ts","sourceRoot":"","sources":["../../../src/src/html/html-injection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAS/D,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gDAAgD;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,wBAAwB,GAChC,MAAM,CA0ER"}
1
+ {"version":3,"file":"html-injection.d.ts","sourceRoot":"","sources":["../../../src/src/html/html-injection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAS/D,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gDAAgD;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,wBAAwB,GAChC,MAAM,CA4ER"}
@@ -56,6 +56,8 @@ export function injectHTMLContent(template, content, metadata, options) {
56
56
  projectId: options.projectId ?? options.slug,
57
57
  pageId: options.pageId ?? options.slug,
58
58
  nonce: options.nonce,
59
+ wsUrl: options.wsUrl,
60
+ yjsGuid: options.yjsGuid,
59
61
  });
60
62
  html = html.replace(/<\/body>/i, `${studioScripts}</body>`);
61
63
  }
@@ -24,6 +24,10 @@ export interface MarkdownHtmlOptions {
24
24
  projectId: string;
25
25
  /** File path of the markdown file. */
26
26
  filePath: string;
27
+ /** Branch ID for Yjs room GUID computation. */
28
+ branchId?: string | null;
29
+ /** Request host for computing the WebSocket URL. */
30
+ requestHost?: string;
27
31
  }
28
32
  /**
29
33
  * Generate a complete HTML document for markdown preview rendering.
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-html-generator.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/preview/markdown-html-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAMrD,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC;IACzB,gDAAgD;IAChD,GAAG,EAAE,GAAG,CAAC;IACT,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAgDD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CA0FzE"}
1
+ {"version":3,"file":"markdown-html-generator.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/preview/markdown-html-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAMrD,oDAAoD;AACpD,MAAM,WAAW,mBAAmB;IAClC,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC;IACzB,gDAAgD;IAChD,GAAG,EAAE,GAAG,CAAC;IACT,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAgED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CA2FzE"}
@@ -25,19 +25,28 @@ function detectTheme(req, url) {
25
25
  * Injected when embedded in Studio (`studio_embed=true`) or for standalone
26
26
  * markdown/MDX pages so the edit button and editor features are available.
27
27
  */
28
- function buildStudioScript(url, projectId, filePath) {
28
+ function buildStudioScript(url, projectId, filePath, branchId, requestHost) {
29
29
  const studioEmbed = url.searchParams.get("studio_embed") === "true";
30
30
  const isMarkdown = /\.mdx?$/i.test(filePath);
31
31
  if (!studioEmbed && !isMarkdown)
32
32
  return "";
33
- const queryProjectId = url.searchParams.get("vf_project_id")?.trim() || "";
33
+ const rawQueryProjectId = url.searchParams.get("vf_project_id")?.trim() || "";
34
+ // Validate query param to prevent path traversal in WebSocket URL
35
+ const queryProjectId = /^[a-zA-Z0-9_-]+$/.test(rawQueryProjectId) ? rawQueryProjectId : "";
34
36
  const queryFileId = url.searchParams.get("vf_file_id")?.trim() || "";
35
37
  const canonicalProjectId = queryProjectId || projectId;
36
38
  const canonicalPageId = queryFileId || filePath;
39
+ // Compute Yjs connection config for the bridge to self-connect
40
+ const wsProtocol = url.protocol === "https:" ? "wss" : "ws";
41
+ const host = requestHost || url.host;
42
+ const wsUrl = `${wsProtocol}://${host}/api/ws/${canonicalProjectId}/yjs`;
43
+ const yjsGuid = branchId ? `${canonicalProjectId}:${branchId}` : canonicalProjectId;
37
44
  return `<script>${generateStudioBridgeScript({
38
45
  projectId: canonicalProjectId,
39
46
  pageId: canonicalPageId,
40
47
  pagePath: filePath,
48
+ wsUrl,
49
+ yjsGuid,
41
50
  })}</script>`;
42
51
  }
43
52
  /**
@@ -48,9 +57,9 @@ function buildStudioScript(url, projectId, filePath) {
48
57
  * studio bridge integration.
49
58
  */
50
59
  export function generateMarkdownHtml(options) {
51
- const { rawHtml, title, description, request, url, projectId, filePath } = options;
60
+ const { rawHtml, title, description, request, url, projectId, filePath, branchId, requestHost } = options;
52
61
  const theme = detectTheme(request, url);
53
- const studioScript = buildStudioScript(url, projectId, filePath);
62
+ const studioScript = buildStudioScript(url, projectId, filePath, branchId, requestHost);
54
63
  const themeAttrs = theme ? ` data-theme="${theme}" style="color-scheme: ${theme};"` : "";
55
64
  return `<!DOCTYPE html>
56
65
  <html lang="en"${themeAttrs}>
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-preview.handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/preview/markdown-preview.handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAmB,aAAa,EAAE,MAAM,aAAa,CAAC;AAgBnG,qBAAa,sBAAuB,SAAQ,WAAW;IACrD,QAAQ,EAAE,eAAe,CAKvB;IAEI,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;YAiEjE,cAAc;CAgF7B"}
1
+ {"version":3,"file":"markdown-preview.handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/preview/markdown-preview.handler.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAmB,aAAa,EAAE,MAAM,aAAa,CAAC;AAgBnG,qBAAa,sBAAuB,SAAQ,WAAW;IACrD,QAAQ,EAAE,eAAe,CAKvB;IAEI,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;YAiEjE,cAAc;CAkF7B"}
@@ -108,6 +108,8 @@ export class MarkdownPreviewHandler extends BaseHandler {
108
108
  url,
109
109
  projectId: ctx.projectSlug || ctx.projectId || "markdown-preview",
110
110
  filePath,
111
+ branchId: ctx.parsedDomain?.branch ?? null,
112
+ requestHost: url.host,
111
113
  });
112
114
  const responseBuilder = this.createResponseBuilder(ctx)
113
115
  .withCache("no-cache")
@@ -1 +1 @@
1
- {"version":3,"file":"endpoints.handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/studio/endpoints.handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EAEf,aAAa,EACd,MAAM,aAAa,CAAC;AAIrB,qBAAa,sBAAuB,SAAQ,WAAW;IACrD,QAAQ,EAAE,eAAe,CAKvB;IAEF,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;CAqB1E"}
1
+ {"version":3,"file":"endpoints.handler.d.ts","sourceRoot":"","sources":["../../../../../src/src/server/handlers/studio/endpoints.handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,OAAO,MAAM,2BAA2B,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EAEf,aAAa,EACd,MAAM,aAAa,CAAC;AAIrB,qBAAa,sBAAuB,SAAQ,WAAW;IACrD,QAAQ,EAAE,eAAe,CAKvB;IAEF,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;CAuB1E"}
@@ -20,7 +20,9 @@ export class StudioEndpointsHandler extends BaseHandler {
20
20
  const projectId = url.searchParams.get("projectId") ?? "";
21
21
  const pageId = url.searchParams.get("pageId") ?? "";
22
22
  const pagePath = url.searchParams.get("pagePath") ?? undefined;
23
- const script = generateStudioBridgeScript({ projectId, pageId, pagePath });
23
+ const wsUrl = url.searchParams.get("wsUrl") ?? undefined;
24
+ const yjsGuid = url.searchParams.get("yjsGuid") ?? undefined;
25
+ const script = generateStudioBridgeScript({ projectId, pageId, pagePath, wsUrl, yjsGuid });
24
26
  const response = builder.withCache("no-cache").javascript(script, HTTP_OK);
25
27
  return Promise.resolve(this.respond(response));
26
28
  }
@@ -2,6 +2,8 @@ export interface StudioBridgeOptions {
2
2
  projectId: string;
3
3
  pageId: string;
4
4
  pagePath?: string;
5
+ wsUrl?: string;
6
+ yjsGuid?: string;
5
7
  debugSkipInit?: boolean;
6
8
  debugExposeInternals?: boolean;
7
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bridge-template.d.ts","sourceRoot":"","sources":["../../../src/src/studio/bridge-template.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAyqI/E"}
1
+ {"version":3,"file":"bridge-template.d.ts","sourceRoot":"","sources":["../../../src/src/studio/bridge-template.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAsvI/E"}
@@ -5,6 +5,8 @@ export function generateStudioBridgeScript(options) {
5
5
  const PROJECT_ID = ${JSON.stringify(options.projectId)};
6
6
  const PAGE_ID = ${JSON.stringify(options.pageId)};
7
7
  const PAGE_PATH = ${JSON.stringify(options.pagePath ?? options.pageId)};
8
+ const WS_URL = ${JSON.stringify(options.wsUrl ?? "")};
9
+ const YJS_GUID = ${JSON.stringify(options.yjsGuid ?? "")};
8
10
  const DEBUG_SKIP_INIT = ${options.debugSkipInit ? "true" : "false"};
9
11
  const DEBUG_EXPOSE_INTERNALS = ${options.debugExposeInternals ? "true" : "false"};
10
12
 
@@ -75,6 +77,8 @@ export function generateStudioBridgeScript(options) {
75
77
  let markdownYText = null;
76
78
  let markdownYjsConnected = false;
77
79
  let markdownYjsSetupId = 0;
80
+ let markdownYjsY = null;
81
+ let markdownPendingSelection = null;
78
82
  const LEXICAL_YJS_ORIGIN = 'lexical-yjs-binding';
79
83
 
80
84
  const MARKDOWN_SLASH_COMMANDS = [
@@ -1466,7 +1470,7 @@ export function generateStudioBridgeScript(options) {
1466
1470
 
1467
1471
  Promise.all([
1468
1472
  import('https://esm.sh/yjs@13.6.28?target=es2022'),
1469
- import('https://esm.sh/y-websocket@2.1.0?target=es2022')
1473
+ import('https://esm.sh/y-websocket@2.1.0?deps=yjs@13.6.28&target=es2022')
1470
1474
  ]).then(function(modules) {
1471
1475
  // Abort if edit mode was closed while imports were loading
1472
1476
  if (setupId !== markdownYjsSetupId) {
@@ -1475,11 +1479,13 @@ export function generateStudioBridgeScript(options) {
1475
1479
 
1476
1480
  var Y = modules[0];
1477
1481
  var WebsocketProvider = modules[1].WebsocketProvider;
1482
+ markdownYjsY = Y;
1478
1483
 
1479
1484
  var doc = new Y.Doc({ guid: config.guid });
1485
+ // Cookie auth: authToken cookie on .veryfront.com is sent automatically
1486
+ // with the WebSocket upgrade request. No explicit token param needed.
1480
1487
  var provider = new WebsocketProvider(config.wsUrl, config.guid, doc, {
1481
- resyncInterval: -1,
1482
- params: { token: config.authToken }
1488
+ resyncInterval: -1
1483
1489
  });
1484
1490
 
1485
1491
  var ytext = doc.getText(config.fileId);
@@ -1504,6 +1510,93 @@ export function generateStudioBridgeScript(options) {
1504
1510
  }
1505
1511
  });
1506
1512
 
1513
+ // Extract user identity from authToken JWT cookie for presence
1514
+ var presenceUser = { id: 'preview-' + Math.random().toString(36).slice(2), name: 'Preview' };
1515
+ try {
1516
+ var cookieMatch = document.cookie.match(/authToken=([^;]+)/);
1517
+ if (cookieMatch) {
1518
+ var parts = cookieMatch[1].split('.');
1519
+ if (parts.length === 3) {
1520
+ var payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
1521
+ if (payload.userId) {
1522
+ presenceUser.id = payload.userId;
1523
+ }
1524
+ if (payload.email) {
1525
+ var local = payload.email.split('@')[0] || '';
1526
+ if (local.includes('.') || local.includes('_')) {
1527
+ presenceUser.name = local.split(/[._]/).map(function(p) { return p.charAt(0).toUpperCase() + p.slice(1); }).join(' ');
1528
+ } else {
1529
+ presenceUser.name = local.charAt(0).toUpperCase() + local.slice(1);
1530
+ }
1531
+ }
1532
+ }
1533
+ }
1534
+ } catch (e) {
1535
+ // Fall back to defaults on any parse error
1536
+ }
1537
+
1538
+ // Set local user on awareness for presence
1539
+ provider.awareness.setLocalStateField('user', {
1540
+ id: presenceUser.id,
1541
+ name: presenceUser.name,
1542
+ color: '#10b981'
1543
+ });
1544
+
1545
+ // Observe awareness for remote presence and selection changes
1546
+ function syncAwareness() {
1547
+ var states = Array.from(provider.awareness.getStates().entries());
1548
+
1549
+ // Sync presence users
1550
+ var users = [];
1551
+ for (var i = 0; i < states.length; i++) {
1552
+ var clientId = states[i][0];
1553
+ var state = states[i][1];
1554
+ var user = state.user;
1555
+ if (!user || typeof user.name !== 'string') {
1556
+ continue;
1557
+ }
1558
+ users.push({
1559
+ id: user.id || String(clientId),
1560
+ name: user.name,
1561
+ color: user.color || '#6b7280',
1562
+ isCurrentUser: clientId === provider.awareness.clientID,
1563
+ isAgent: user.isAgent || false
1564
+ });
1565
+ }
1566
+ setMarkdownPresence(users);
1567
+
1568
+ // Sync remote selections
1569
+ var selections = [];
1570
+ for (var j = 0; j < states.length; j++) {
1571
+ var cId = states[j][0];
1572
+ var st = states[j][1];
1573
+ var u = st.user;
1574
+ var ranges = st.selection;
1575
+ if (!u || !Array.isArray(ranges) || ranges.length === 0) {
1576
+ continue;
1577
+ }
1578
+ for (var k = 0; k < ranges.length; k++) {
1579
+ var range = ranges[k];
1580
+ var anchorPos = Y.createAbsolutePositionFromRelativePosition(range.anchor, doc);
1581
+ var markerPos = Y.createAbsolutePositionFromRelativePosition(range.marker, doc);
1582
+ if (!anchorPos || !markerPos || anchorPos.type !== ytext || markerPos.type !== ytext) {
1583
+ continue;
1584
+ }
1585
+ selections.push({
1586
+ id: u.id || String(cId),
1587
+ name: u.name || 'Anonymous',
1588
+ color: u.color || '#6b7280',
1589
+ isCurrentUser: cId === provider.awareness.clientID,
1590
+ start: Math.min(anchorPos.index, markerPos.index),
1591
+ end: Math.max(anchorPos.index, markerPos.index)
1592
+ });
1593
+ }
1594
+ }
1595
+ setMarkdownSelections(selections);
1596
+ }
1597
+
1598
+ provider.awareness.on('change', syncAwareness);
1599
+
1507
1600
  provider.on('sync', function(synced) {
1508
1601
  if (synced && !markdownYjsConnected) {
1509
1602
  markdownYjsConnected = true;
@@ -1517,6 +1610,18 @@ export function generateStudioBridgeScript(options) {
1517
1610
  applyMarkdownContent(ytextContent);
1518
1611
  }
1519
1612
 
1613
+ // Replay any selection queued before Yjs was ready
1614
+ if (markdownPendingSelection) {
1615
+ var ps = markdownPendingSelection;
1616
+ markdownPendingSelection = null;
1617
+ var cs = Math.max(0, Math.min(ytext.length, ps.start));
1618
+ var ce = Math.max(0, Math.min(ytext.length, ps.end));
1619
+ provider.awareness.setLocalStateField('selection', [{
1620
+ anchor: Y.createRelativePositionFromTypeIndex(ytext, cs),
1621
+ marker: Y.createRelativePositionFromTypeIndex(ytext, ce)
1622
+ }]);
1623
+ }
1624
+
1520
1625
  // Observe Y.Text for remote changes (from other users / Monaco)
1521
1626
  ytext.observe(function(event) {
1522
1627
  if (event.transaction.origin === LEXICAL_YJS_ORIGIN) {
@@ -1529,6 +1634,9 @@ export function generateStudioBridgeScript(options) {
1529
1634
  applyMarkdownContent(fullContent);
1530
1635
  });
1531
1636
 
1637
+ // Initial awareness sync after Yjs is connected
1638
+ syncAwareness();
1639
+
1532
1640
  console.debug('[StudioBridge] Yjs synced, bound to Y.Text for fileId:', config.fileId);
1533
1641
  }
1534
1642
  });
@@ -1550,6 +1658,7 @@ export function generateStudioBridgeScript(options) {
1550
1658
  }
1551
1659
  markdownYText = null;
1552
1660
  markdownYjsConnected = false;
1661
+ markdownYjsY = null;
1553
1662
  }
1554
1663
 
1555
1664
  function getTextOffsetWithinRoot(root, targetNode, targetOffset) {
@@ -2637,27 +2746,26 @@ export function generateStudioBridgeScript(options) {
2637
2746
  const start = editorOffsetToSourceOffset(selection.start, 'start');
2638
2747
  const end = editorOffsetToSourceOffset(selection.end, 'end');
2639
2748
 
2640
- postToStudio({
2641
- action: 'markdownSelectionChange',
2642
- fileId: markdownFileId,
2643
- filePath: PAGE_PATH,
2644
- start: start,
2645
- end: end
2646
- });
2749
+ // Set local selection on Yjs awareness directly
2750
+ if (markdownYjsConnected && markdownYText && markdownYjsY && markdownYProvider) {
2751
+ var clampedStart = Math.max(0, Math.min(markdownYText.length, start));
2752
+ var clampedEnd = Math.max(0, Math.min(markdownYText.length, end));
2753
+ markdownYProvider.awareness.setLocalStateField('selection', [{
2754
+ anchor: markdownYjsY.createRelativePositionFromTypeIndex(markdownYText, clampedStart),
2755
+ marker: markdownYjsY.createRelativePositionFromTypeIndex(markdownYText, clampedEnd)
2756
+ }]);
2757
+ markdownPendingSelection = null;
2758
+ } else {
2759
+ // Queue selection for replay after Yjs connects
2760
+ markdownPendingSelection = { start: start, end: end };
2761
+ }
2647
2762
  }, 80);
2648
2763
  }
2649
2764
 
2650
2765
  function clearMarkdownSelectionSync() {
2651
- if (!markdownFileId) {
2652
- return;
2766
+ if (markdownYProvider) {
2767
+ markdownYProvider.awareness.setLocalStateField('selection', null);
2653
2768
  }
2654
- postToStudio({
2655
- action: 'markdownSelectionChange',
2656
- fileId: markdownFileId,
2657
- filePath: PAGE_PATH,
2658
- start: -1,
2659
- end: -1
2660
- });
2661
2769
  }
2662
2770
 
2663
2771
  function clearMarkdownSelectionOverlay() {
@@ -3053,9 +3161,8 @@ export function generateStudioBridgeScript(options) {
3053
3161
  markdownHasUnsavedChanges = true;
3054
3162
  if (markdownYjsConnected) {
3055
3163
  syncLocalChangeToYText(fullContent);
3056
- } else {
3057
- scheduleMarkdownSync(fullContent);
3058
3164
  }
3165
+ scheduleMarkdownSync(fullContent);
3059
3166
  scheduleMarkdownSelectionOverlayRender();
3060
3167
  }
3061
3168
 
@@ -3216,7 +3323,7 @@ export function generateStudioBridgeScript(options) {
3216
3323
  return;
3217
3324
  }
3218
3325
 
3219
- if (markdownLexicalApi && (markdownLexicalRenderedContent === content || markdownCurrentContent === content)) {
3326
+ if (markdownLexicalApi && markdownLexicalRenderedContent === content) {
3220
3327
  console.debug('[StudioBridge] applyMarkdownContent: skipped (content unchanged)');
3221
3328
  markdownCurrentContent = content;
3222
3329
  scheduleMarkdownSelectionOverlayRender();
@@ -3846,6 +3953,15 @@ export function generateStudioBridgeScript(options) {
3846
3953
  scheduleMarkdownSlashMenuUpdate();
3847
3954
  scheduleMarkdownInlineToolbarUpdate();
3848
3955
  postMarkdownEditorReady();
3956
+
3957
+ // Self-connect to Yjs when server-injected config is available
3958
+ if (WS_URL && YJS_GUID && !markdownYDoc) {
3959
+ setupMarkdownYjsConnection({
3960
+ wsUrl: WS_URL,
3961
+ guid: YJS_GUID,
3962
+ fileId: markdownFileId
3963
+ });
3964
+ }
3849
3965
  } else {
3850
3966
  markdownBody.style.display = '';
3851
3967
  if (markdownEditorRoot) {
@@ -4081,37 +4197,6 @@ export function generateStudioBridgeScript(options) {
4081
4197
  if (!inspectMode) showHoverOverlay(message.id);
4082
4198
  return;
4083
4199
 
4084
- case 'setMarkdownContent':
4085
- if (!isMarkdownPage()) {
4086
- return;
4087
- }
4088
- if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
4089
- return;
4090
- }
4091
- if (markdownYjsConnected) {
4092
- return;
4093
- }
4094
- applyMarkdownContent(message.content || '');
4095
- return;
4096
-
4097
- case 'initYjsConnection':
4098
- if (!isMarkdownPage()) {
4099
- return;
4100
- }
4101
- if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
4102
- return;
4103
- }
4104
- if (message.initialContent) {
4105
- applyMarkdownContent(message.initialContent);
4106
- }
4107
- setupMarkdownYjsConnection({
4108
- wsUrl: message.wsUrl,
4109
- guid: message.guid,
4110
- fileId: message.fileId || markdownFileId,
4111
- authToken: message.authToken
4112
- });
4113
- return;
4114
-
4115
4200
  case 'setMarkdownPersistState':
4116
4201
  if (!isMarkdownPage()) {
4117
4202
  return;
@@ -4128,26 +4213,6 @@ export function generateStudioBridgeScript(options) {
4128
4213
  }
4129
4214
  return;
4130
4215
 
4131
- case 'setMarkdownPresence':
4132
- if (!isMarkdownPage()) {
4133
- return;
4134
- }
4135
- if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
4136
- return;
4137
- }
4138
- setMarkdownPresence(message.users);
4139
- return;
4140
-
4141
- case 'setMarkdownSelections':
4142
- if (!isMarkdownPage()) {
4143
- return;
4144
- }
4145
- if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
4146
- return;
4147
- }
4148
- setMarkdownSelections(message.selections);
4149
- return;
4150
-
4151
4216
  case 'screenshot':
4152
4217
  (async function() {
4153
4218
  if (message.multipleSections) {
@@ -4205,47 +4270,59 @@ export function generateStudioBridgeScript(options) {
4205
4270
  function init() {
4206
4271
  const params = new URLSearchParams(window.location.search);
4207
4272
  const studioEmbed = params.get('studio_embed') === 'true';
4273
+ const isStandalone = window.parent === window && !studioEmbed;
4208
4274
 
4209
- if (window.parent === window && !studioEmbed) {
4210
- console.debug('[StudioBridge] Not in iframe and not studio_embed mode, skipping initialization');
4211
- return;
4275
+ if (isStandalone) {
4276
+ // Allow standalone markdown editing when WS_URL is available (server-injected Yjs config)
4277
+ if (!WS_URL) {
4278
+ console.debug('[StudioBridge] Not in iframe and not studio_embed mode, skipping initialization');
4279
+ return;
4280
+ }
4212
4281
  }
4213
4282
 
4214
4283
  console.debug('[StudioBridge] Initializing...');
4215
4284
 
4216
- injectOverlayStyles();
4217
- hoverOverlay = createOverlay('hover');
4218
- selectionOverlay = createOverlay('selection');
4285
+ // Only set up Studio interaction features when embedded in Studio
4286
+ if (!isStandalone) {
4287
+ injectOverlayStyles();
4288
+ hoverOverlay = createOverlay('hover');
4289
+ selectionOverlay = createOverlay('selection');
4290
+
4291
+ setupConsoleCapture();
4292
+ setupErrorHandling();
4293
+ setupInspectMode();
4294
+ }
4219
4295
 
4220
- setupConsoleCapture();
4221
- setupErrorHandling();
4222
- setupInspectMode();
4223
4296
  setupMarkdownEditor(params);
4224
4297
 
4225
4298
  window.addEventListener('message', handleStudioMessage);
4226
4299
 
4227
- // IMPORTANT: notifyAppLoaded() must be called BEFORE setupMutationObserver()
4228
- // because notifyAppLoaded sends onPageTransitionEnd which sets previewId,
4229
- // and treeUpdated (from setupMutationObserver) requires previewId to be set
4230
- if (document.readyState === 'loading') {
4231
- document.addEventListener('DOMContentLoaded', function() {
4300
+ if (!isStandalone) {
4301
+ // IMPORTANT: notifyAppLoaded() must be called BEFORE setupMutationObserver()
4302
+ // because notifyAppLoaded sends onPageTransitionEnd which sets previewId,
4303
+ // and treeUpdated (from setupMutationObserver) requires previewId to be set
4304
+ if (document.readyState === 'loading') {
4305
+ document.addEventListener('DOMContentLoaded', function() {
4306
+ notifyAppLoaded();
4307
+ setupMutationObserver();
4308
+ });
4309
+ } else {
4232
4310
  notifyAppLoaded();
4233
4311
  setupMutationObserver();
4234
- });
4235
- } else {
4236
- notifyAppLoaded();
4237
- setupMutationObserver();
4238
- }
4312
+ }
4239
4313
 
4240
- window.addEventListener('beforeunload', notifyAppUnloaded);
4314
+ window.addEventListener('beforeunload', notifyAppUnloaded);
4315
+ }
4241
4316
 
4242
4317
  const colorMode = params.get('color_mode');
4243
4318
  if (colorMode) setColorMode(colorMode);
4244
4319
 
4245
- const inspectModeParam = params.get('inspect_mode');
4246
- if (inspectModeParam === 'true') {
4247
- inspectMode = true;
4248
- console.debug('[StudioBridge] Inspect mode enabled from query param');
4320
+ if (!isStandalone) {
4321
+ const inspectModeParam = params.get('inspect_mode');
4322
+ if (inspectModeParam === 'true') {
4323
+ inspectMode = true;
4324
+ console.debug('[StudioBridge] Inspect mode enabled from query param');
4325
+ }
4249
4326
  }
4250
4327
 
4251
4328
  console.debug('[StudioBridge] Initialized successfully');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -59,6 +59,10 @@ export interface StudioScriptOptions {
59
59
  nonce?: string;
60
60
  /** Hash of source code for sync detection with Navigator tree */
61
61
  sourceHash?: string;
62
+ /** WebSocket URL for direct Yjs connection from the bridge */
63
+ wsUrl?: string;
64
+ /** Yjs document GUID for the bridge to join the same room */
65
+ yjsGuid?: string;
62
66
  }
63
67
 
64
68
  export function getStudioScripts(options: StudioScriptOptions): string {
@@ -68,6 +72,8 @@ export function getStudioScripts(options: StudioScriptOptions): string {
68
72
  projectId: options.projectId,
69
73
  pageId: options.pageId,
70
74
  ...(options.pagePath ? { pagePath: options.pagePath } : {}),
75
+ ...(options.wsUrl ? { wsUrl: options.wsUrl } : {}),
76
+ ...(options.yjsGuid ? { yjsGuid: options.yjsGuid } : {}),
71
77
  }).toString();
72
78
 
73
79
  const sourceHashScript = options.sourceHash
@@ -23,6 +23,10 @@ export interface InjectHTMLContentOptions {
23
23
  pageId?: string;
24
24
  /** CSP nonce */
25
25
  nonce?: string;
26
+ /** WebSocket URL for direct Yjs connection from the bridge */
27
+ wsUrl?: string;
28
+ /** Yjs document GUID for the bridge to join the same room */
29
+ yjsGuid?: string;
26
30
  }
27
31
 
28
32
  export function injectHTMLContent(
@@ -99,6 +103,8 @@ export function injectHTMLContent(
99
103
  projectId: options.projectId ?? options.slug,
100
104
  pageId: options.pageId ?? options.slug,
101
105
  nonce: options.nonce,
106
+ wsUrl: options.wsUrl,
107
+ yjsGuid: options.yjsGuid,
102
108
  });
103
109
  html = html.replace(/<\/body>/i, `${studioScripts}</body>`);
104
110
  }
@@ -29,6 +29,10 @@ export interface MarkdownHtmlOptions {
29
29
  projectId: string;
30
30
  /** File path of the markdown file. */
31
31
  filePath: string;
32
+ /** Branch ID for Yjs room GUID computation. */
33
+ branchId?: string | null;
34
+ /** Request host for computing the WebSocket URL. */
35
+ requestHost?: string;
32
36
  }
33
37
 
34
38
  /**
@@ -58,21 +62,37 @@ function detectTheme(req: dntShim.Request, url: URL): "light" | "dark" | null {
58
62
  * Injected when embedded in Studio (`studio_embed=true`) or for standalone
59
63
  * markdown/MDX pages so the edit button and editor features are available.
60
64
  */
61
- function buildStudioScript(url: URL, projectId: string, filePath: string): string {
65
+ function buildStudioScript(
66
+ url: URL,
67
+ projectId: string,
68
+ filePath: string,
69
+ branchId?: string | null,
70
+ requestHost?: string,
71
+ ): string {
62
72
  const studioEmbed = url.searchParams.get("studio_embed") === "true";
63
73
  const isMarkdown = /\.mdx?$/i.test(filePath);
64
74
  if (!studioEmbed && !isMarkdown) return "";
65
75
 
66
- const queryProjectId = url.searchParams.get("vf_project_id")?.trim() || "";
76
+ const rawQueryProjectId = url.searchParams.get("vf_project_id")?.trim() || "";
77
+ // Validate query param to prevent path traversal in WebSocket URL
78
+ const queryProjectId = /^[a-zA-Z0-9_-]+$/.test(rawQueryProjectId) ? rawQueryProjectId : "";
67
79
  const queryFileId = url.searchParams.get("vf_file_id")?.trim() || "";
68
80
  const canonicalProjectId = queryProjectId || projectId;
69
81
  const canonicalPageId = queryFileId || filePath;
70
82
 
83
+ // Compute Yjs connection config for the bridge to self-connect
84
+ const wsProtocol = url.protocol === "https:" ? "wss" : "ws";
85
+ const host = requestHost || url.host;
86
+ const wsUrl = `${wsProtocol}://${host}/api/ws/${canonicalProjectId}/yjs`;
87
+ const yjsGuid = branchId ? `${canonicalProjectId}:${branchId}` : canonicalProjectId;
88
+
71
89
  return `<script>${
72
90
  generateStudioBridgeScript({
73
91
  projectId: canonicalProjectId,
74
92
  pageId: canonicalPageId,
75
93
  pagePath: filePath,
94
+ wsUrl,
95
+ yjsGuid,
76
96
  })
77
97
  }</script>`;
78
98
  }
@@ -85,10 +105,11 @@ function buildStudioScript(url: URL, projectId: string, filePath: string): strin
85
105
  * studio bridge integration.
86
106
  */
87
107
  export function generateMarkdownHtml(options: MarkdownHtmlOptions): string {
88
- const { rawHtml, title, description, request, url, projectId, filePath } = options;
108
+ const { rawHtml, title, description, request, url, projectId, filePath, branchId, requestHost } =
109
+ options;
89
110
 
90
111
  const theme = detectTheme(request, url);
91
- const studioScript = buildStudioScript(url, projectId, filePath);
112
+ const studioScript = buildStudioScript(url, projectId, filePath, branchId, requestHost);
92
113
  const themeAttrs = theme ? ` data-theme="${theme}" style="color-scheme: ${theme};"` : "";
93
114
 
94
115
  return `<!DOCTYPE html>
@@ -159,6 +159,8 @@ export class MarkdownPreviewHandler extends BaseHandler {
159
159
  url,
160
160
  projectId: ctx.projectSlug || ctx.projectId || "markdown-preview",
161
161
  filePath,
162
+ branchId: ctx.parsedDomain?.branch ?? null,
163
+ requestHost: url.host,
162
164
  });
163
165
 
164
166
  const responseBuilder = this.createResponseBuilder(ctx)
@@ -38,8 +38,10 @@ export class StudioEndpointsHandler extends BaseHandler {
38
38
  const projectId = url.searchParams.get("projectId") ?? "";
39
39
  const pageId = url.searchParams.get("pageId") ?? "";
40
40
  const pagePath = url.searchParams.get("pagePath") ?? undefined;
41
+ const wsUrl = url.searchParams.get("wsUrl") ?? undefined;
42
+ const yjsGuid = url.searchParams.get("yjsGuid") ?? undefined;
41
43
 
42
- const script = generateStudioBridgeScript({ projectId, pageId, pagePath });
44
+ const script = generateStudioBridgeScript({ projectId, pageId, pagePath, wsUrl, yjsGuid });
43
45
  const response = builder.withCache("no-cache").javascript(script, HTTP_OK);
44
46
 
45
47
  return Promise.resolve(this.respond(response));
@@ -2,6 +2,8 @@ export interface StudioBridgeOptions {
2
2
  projectId: string;
3
3
  pageId: string;
4
4
  pagePath?: string;
5
+ wsUrl?: string;
6
+ yjsGuid?: string;
5
7
  debugSkipInit?: boolean;
6
8
  debugExposeInternals?: boolean;
7
9
  }
@@ -13,6 +15,8 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
13
15
  const PROJECT_ID = ${JSON.stringify(options.projectId)};
14
16
  const PAGE_ID = ${JSON.stringify(options.pageId)};
15
17
  const PAGE_PATH = ${JSON.stringify(options.pagePath ?? options.pageId)};
18
+ const WS_URL = ${JSON.stringify(options.wsUrl ?? "")};
19
+ const YJS_GUID = ${JSON.stringify(options.yjsGuid ?? "")};
16
20
  const DEBUG_SKIP_INIT = ${options.debugSkipInit ? "true" : "false"};
17
21
  const DEBUG_EXPOSE_INTERNALS = ${options.debugExposeInternals ? "true" : "false"};
18
22
 
@@ -83,6 +87,8 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
83
87
  let markdownYText = null;
84
88
  let markdownYjsConnected = false;
85
89
  let markdownYjsSetupId = 0;
90
+ let markdownYjsY = null;
91
+ let markdownPendingSelection = null;
86
92
  const LEXICAL_YJS_ORIGIN = 'lexical-yjs-binding';
87
93
 
88
94
  const MARKDOWN_SLASH_COMMANDS = [
@@ -1474,7 +1480,7 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
1474
1480
 
1475
1481
  Promise.all([
1476
1482
  import('https://esm.sh/yjs@13.6.28?target=es2022'),
1477
- import('https://esm.sh/y-websocket@2.1.0?target=es2022')
1483
+ import('https://esm.sh/y-websocket@2.1.0?deps=yjs@13.6.28&target=es2022')
1478
1484
  ]).then(function(modules) {
1479
1485
  // Abort if edit mode was closed while imports were loading
1480
1486
  if (setupId !== markdownYjsSetupId) {
@@ -1483,11 +1489,13 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
1483
1489
 
1484
1490
  var Y = modules[0];
1485
1491
  var WebsocketProvider = modules[1].WebsocketProvider;
1492
+ markdownYjsY = Y;
1486
1493
 
1487
1494
  var doc = new Y.Doc({ guid: config.guid });
1495
+ // Cookie auth: authToken cookie on .veryfront.com is sent automatically
1496
+ // with the WebSocket upgrade request. No explicit token param needed.
1488
1497
  var provider = new WebsocketProvider(config.wsUrl, config.guid, doc, {
1489
- resyncInterval: -1,
1490
- params: { token: config.authToken }
1498
+ resyncInterval: -1
1491
1499
  });
1492
1500
 
1493
1501
  var ytext = doc.getText(config.fileId);
@@ -1512,6 +1520,93 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
1512
1520
  }
1513
1521
  });
1514
1522
 
1523
+ // Extract user identity from authToken JWT cookie for presence
1524
+ var presenceUser = { id: 'preview-' + Math.random().toString(36).slice(2), name: 'Preview' };
1525
+ try {
1526
+ var cookieMatch = document.cookie.match(/authToken=([^;]+)/);
1527
+ if (cookieMatch) {
1528
+ var parts = cookieMatch[1].split('.');
1529
+ if (parts.length === 3) {
1530
+ var payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
1531
+ if (payload.userId) {
1532
+ presenceUser.id = payload.userId;
1533
+ }
1534
+ if (payload.email) {
1535
+ var local = payload.email.split('@')[0] || '';
1536
+ if (local.includes('.') || local.includes('_')) {
1537
+ presenceUser.name = local.split(/[._]/).map(function(p) { return p.charAt(0).toUpperCase() + p.slice(1); }).join(' ');
1538
+ } else {
1539
+ presenceUser.name = local.charAt(0).toUpperCase() + local.slice(1);
1540
+ }
1541
+ }
1542
+ }
1543
+ }
1544
+ } catch (e) {
1545
+ // Fall back to defaults on any parse error
1546
+ }
1547
+
1548
+ // Set local user on awareness for presence
1549
+ provider.awareness.setLocalStateField('user', {
1550
+ id: presenceUser.id,
1551
+ name: presenceUser.name,
1552
+ color: '#10b981'
1553
+ });
1554
+
1555
+ // Observe awareness for remote presence and selection changes
1556
+ function syncAwareness() {
1557
+ var states = Array.from(provider.awareness.getStates().entries());
1558
+
1559
+ // Sync presence users
1560
+ var users = [];
1561
+ for (var i = 0; i < states.length; i++) {
1562
+ var clientId = states[i][0];
1563
+ var state = states[i][1];
1564
+ var user = state.user;
1565
+ if (!user || typeof user.name !== 'string') {
1566
+ continue;
1567
+ }
1568
+ users.push({
1569
+ id: user.id || String(clientId),
1570
+ name: user.name,
1571
+ color: user.color || '#6b7280',
1572
+ isCurrentUser: clientId === provider.awareness.clientID,
1573
+ isAgent: user.isAgent || false
1574
+ });
1575
+ }
1576
+ setMarkdownPresence(users);
1577
+
1578
+ // Sync remote selections
1579
+ var selections = [];
1580
+ for (var j = 0; j < states.length; j++) {
1581
+ var cId = states[j][0];
1582
+ var st = states[j][1];
1583
+ var u = st.user;
1584
+ var ranges = st.selection;
1585
+ if (!u || !Array.isArray(ranges) || ranges.length === 0) {
1586
+ continue;
1587
+ }
1588
+ for (var k = 0; k < ranges.length; k++) {
1589
+ var range = ranges[k];
1590
+ var anchorPos = Y.createAbsolutePositionFromRelativePosition(range.anchor, doc);
1591
+ var markerPos = Y.createAbsolutePositionFromRelativePosition(range.marker, doc);
1592
+ if (!anchorPos || !markerPos || anchorPos.type !== ytext || markerPos.type !== ytext) {
1593
+ continue;
1594
+ }
1595
+ selections.push({
1596
+ id: u.id || String(cId),
1597
+ name: u.name || 'Anonymous',
1598
+ color: u.color || '#6b7280',
1599
+ isCurrentUser: cId === provider.awareness.clientID,
1600
+ start: Math.min(anchorPos.index, markerPos.index),
1601
+ end: Math.max(anchorPos.index, markerPos.index)
1602
+ });
1603
+ }
1604
+ }
1605
+ setMarkdownSelections(selections);
1606
+ }
1607
+
1608
+ provider.awareness.on('change', syncAwareness);
1609
+
1515
1610
  provider.on('sync', function(synced) {
1516
1611
  if (synced && !markdownYjsConnected) {
1517
1612
  markdownYjsConnected = true;
@@ -1525,6 +1620,18 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
1525
1620
  applyMarkdownContent(ytextContent);
1526
1621
  }
1527
1622
 
1623
+ // Replay any selection queued before Yjs was ready
1624
+ if (markdownPendingSelection) {
1625
+ var ps = markdownPendingSelection;
1626
+ markdownPendingSelection = null;
1627
+ var cs = Math.max(0, Math.min(ytext.length, ps.start));
1628
+ var ce = Math.max(0, Math.min(ytext.length, ps.end));
1629
+ provider.awareness.setLocalStateField('selection', [{
1630
+ anchor: Y.createRelativePositionFromTypeIndex(ytext, cs),
1631
+ marker: Y.createRelativePositionFromTypeIndex(ytext, ce)
1632
+ }]);
1633
+ }
1634
+
1528
1635
  // Observe Y.Text for remote changes (from other users / Monaco)
1529
1636
  ytext.observe(function(event) {
1530
1637
  if (event.transaction.origin === LEXICAL_YJS_ORIGIN) {
@@ -1537,6 +1644,9 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
1537
1644
  applyMarkdownContent(fullContent);
1538
1645
  });
1539
1646
 
1647
+ // Initial awareness sync after Yjs is connected
1648
+ syncAwareness();
1649
+
1540
1650
  console.debug('[StudioBridge] Yjs synced, bound to Y.Text for fileId:', config.fileId);
1541
1651
  }
1542
1652
  });
@@ -1558,6 +1668,7 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
1558
1668
  }
1559
1669
  markdownYText = null;
1560
1670
  markdownYjsConnected = false;
1671
+ markdownYjsY = null;
1561
1672
  }
1562
1673
 
1563
1674
  function getTextOffsetWithinRoot(root, targetNode, targetOffset) {
@@ -2645,27 +2756,26 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
2645
2756
  const start = editorOffsetToSourceOffset(selection.start, 'start');
2646
2757
  const end = editorOffsetToSourceOffset(selection.end, 'end');
2647
2758
 
2648
- postToStudio({
2649
- action: 'markdownSelectionChange',
2650
- fileId: markdownFileId,
2651
- filePath: PAGE_PATH,
2652
- start: start,
2653
- end: end
2654
- });
2759
+ // Set local selection on Yjs awareness directly
2760
+ if (markdownYjsConnected && markdownYText && markdownYjsY && markdownYProvider) {
2761
+ var clampedStart = Math.max(0, Math.min(markdownYText.length, start));
2762
+ var clampedEnd = Math.max(0, Math.min(markdownYText.length, end));
2763
+ markdownYProvider.awareness.setLocalStateField('selection', [{
2764
+ anchor: markdownYjsY.createRelativePositionFromTypeIndex(markdownYText, clampedStart),
2765
+ marker: markdownYjsY.createRelativePositionFromTypeIndex(markdownYText, clampedEnd)
2766
+ }]);
2767
+ markdownPendingSelection = null;
2768
+ } else {
2769
+ // Queue selection for replay after Yjs connects
2770
+ markdownPendingSelection = { start: start, end: end };
2771
+ }
2655
2772
  }, 80);
2656
2773
  }
2657
2774
 
2658
2775
  function clearMarkdownSelectionSync() {
2659
- if (!markdownFileId) {
2660
- return;
2776
+ if (markdownYProvider) {
2777
+ markdownYProvider.awareness.setLocalStateField('selection', null);
2661
2778
  }
2662
- postToStudio({
2663
- action: 'markdownSelectionChange',
2664
- fileId: markdownFileId,
2665
- filePath: PAGE_PATH,
2666
- start: -1,
2667
- end: -1
2668
- });
2669
2779
  }
2670
2780
 
2671
2781
  function clearMarkdownSelectionOverlay() {
@@ -3061,9 +3171,8 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
3061
3171
  markdownHasUnsavedChanges = true;
3062
3172
  if (markdownYjsConnected) {
3063
3173
  syncLocalChangeToYText(fullContent);
3064
- } else {
3065
- scheduleMarkdownSync(fullContent);
3066
3174
  }
3175
+ scheduleMarkdownSync(fullContent);
3067
3176
  scheduleMarkdownSelectionOverlayRender();
3068
3177
  }
3069
3178
 
@@ -3224,7 +3333,7 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
3224
3333
  return;
3225
3334
  }
3226
3335
 
3227
- if (markdownLexicalApi && (markdownLexicalRenderedContent === content || markdownCurrentContent === content)) {
3336
+ if (markdownLexicalApi && markdownLexicalRenderedContent === content) {
3228
3337
  console.debug('[StudioBridge] applyMarkdownContent: skipped (content unchanged)');
3229
3338
  markdownCurrentContent = content;
3230
3339
  scheduleMarkdownSelectionOverlayRender();
@@ -3854,6 +3963,15 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
3854
3963
  scheduleMarkdownSlashMenuUpdate();
3855
3964
  scheduleMarkdownInlineToolbarUpdate();
3856
3965
  postMarkdownEditorReady();
3966
+
3967
+ // Self-connect to Yjs when server-injected config is available
3968
+ if (WS_URL && YJS_GUID && !markdownYDoc) {
3969
+ setupMarkdownYjsConnection({
3970
+ wsUrl: WS_URL,
3971
+ guid: YJS_GUID,
3972
+ fileId: markdownFileId
3973
+ });
3974
+ }
3857
3975
  } else {
3858
3976
  markdownBody.style.display = '';
3859
3977
  if (markdownEditorRoot) {
@@ -4089,37 +4207,6 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
4089
4207
  if (!inspectMode) showHoverOverlay(message.id);
4090
4208
  return;
4091
4209
 
4092
- case 'setMarkdownContent':
4093
- if (!isMarkdownPage()) {
4094
- return;
4095
- }
4096
- if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
4097
- return;
4098
- }
4099
- if (markdownYjsConnected) {
4100
- return;
4101
- }
4102
- applyMarkdownContent(message.content || '');
4103
- return;
4104
-
4105
- case 'initYjsConnection':
4106
- if (!isMarkdownPage()) {
4107
- return;
4108
- }
4109
- if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
4110
- return;
4111
- }
4112
- if (message.initialContent) {
4113
- applyMarkdownContent(message.initialContent);
4114
- }
4115
- setupMarkdownYjsConnection({
4116
- wsUrl: message.wsUrl,
4117
- guid: message.guid,
4118
- fileId: message.fileId || markdownFileId,
4119
- authToken: message.authToken
4120
- });
4121
- return;
4122
-
4123
4210
  case 'setMarkdownPersistState':
4124
4211
  if (!isMarkdownPage()) {
4125
4212
  return;
@@ -4136,26 +4223,6 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
4136
4223
  }
4137
4224
  return;
4138
4225
 
4139
- case 'setMarkdownPresence':
4140
- if (!isMarkdownPage()) {
4141
- return;
4142
- }
4143
- if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
4144
- return;
4145
- }
4146
- setMarkdownPresence(message.users);
4147
- return;
4148
-
4149
- case 'setMarkdownSelections':
4150
- if (!isMarkdownPage()) {
4151
- return;
4152
- }
4153
- if (message.fileId && markdownFileId && message.fileId !== markdownFileId) {
4154
- return;
4155
- }
4156
- setMarkdownSelections(message.selections);
4157
- return;
4158
-
4159
4226
  case 'screenshot':
4160
4227
  (async function() {
4161
4228
  if (message.multipleSections) {
@@ -4213,47 +4280,59 @@ export function generateStudioBridgeScript(options: StudioBridgeOptions): string
4213
4280
  function init() {
4214
4281
  const params = new URLSearchParams(window.location.search);
4215
4282
  const studioEmbed = params.get('studio_embed') === 'true';
4283
+ const isStandalone = window.parent === window && !studioEmbed;
4216
4284
 
4217
- if (window.parent === window && !studioEmbed) {
4218
- console.debug('[StudioBridge] Not in iframe and not studio_embed mode, skipping initialization');
4219
- return;
4285
+ if (isStandalone) {
4286
+ // Allow standalone markdown editing when WS_URL is available (server-injected Yjs config)
4287
+ if (!WS_URL) {
4288
+ console.debug('[StudioBridge] Not in iframe and not studio_embed mode, skipping initialization');
4289
+ return;
4290
+ }
4220
4291
  }
4221
4292
 
4222
4293
  console.debug('[StudioBridge] Initializing...');
4223
4294
 
4224
- injectOverlayStyles();
4225
- hoverOverlay = createOverlay('hover');
4226
- selectionOverlay = createOverlay('selection');
4295
+ // Only set up Studio interaction features when embedded in Studio
4296
+ if (!isStandalone) {
4297
+ injectOverlayStyles();
4298
+ hoverOverlay = createOverlay('hover');
4299
+ selectionOverlay = createOverlay('selection');
4300
+
4301
+ setupConsoleCapture();
4302
+ setupErrorHandling();
4303
+ setupInspectMode();
4304
+ }
4227
4305
 
4228
- setupConsoleCapture();
4229
- setupErrorHandling();
4230
- setupInspectMode();
4231
4306
  setupMarkdownEditor(params);
4232
4307
 
4233
4308
  window.addEventListener('message', handleStudioMessage);
4234
4309
 
4235
- // IMPORTANT: notifyAppLoaded() must be called BEFORE setupMutationObserver()
4236
- // because notifyAppLoaded sends onPageTransitionEnd which sets previewId,
4237
- // and treeUpdated (from setupMutationObserver) requires previewId to be set
4238
- if (document.readyState === 'loading') {
4239
- document.addEventListener('DOMContentLoaded', function() {
4310
+ if (!isStandalone) {
4311
+ // IMPORTANT: notifyAppLoaded() must be called BEFORE setupMutationObserver()
4312
+ // because notifyAppLoaded sends onPageTransitionEnd which sets previewId,
4313
+ // and treeUpdated (from setupMutationObserver) requires previewId to be set
4314
+ if (document.readyState === 'loading') {
4315
+ document.addEventListener('DOMContentLoaded', function() {
4316
+ notifyAppLoaded();
4317
+ setupMutationObserver();
4318
+ });
4319
+ } else {
4240
4320
  notifyAppLoaded();
4241
4321
  setupMutationObserver();
4242
- });
4243
- } else {
4244
- notifyAppLoaded();
4245
- setupMutationObserver();
4246
- }
4322
+ }
4247
4323
 
4248
- window.addEventListener('beforeunload', notifyAppUnloaded);
4324
+ window.addEventListener('beforeunload', notifyAppUnloaded);
4325
+ }
4249
4326
 
4250
4327
  const colorMode = params.get('color_mode');
4251
4328
  if (colorMode) setColorMode(colorMode);
4252
4329
 
4253
- const inspectModeParam = params.get('inspect_mode');
4254
- if (inspectModeParam === 'true') {
4255
- inspectMode = true;
4256
- console.debug('[StudioBridge] Inspect mode enabled from query param');
4330
+ if (!isStandalone) {
4331
+ const inspectModeParam = params.get('inspect_mode');
4332
+ if (inspectModeParam === 'true') {
4333
+ inspectMode = true;
4334
+ console.debug('[StudioBridge] Inspect mode enabled from query param');
4335
+ }
4257
4336
  }
4258
4337
 
4259
4338
  console.debug('[StudioBridge] Initialized successfully');