@malloydata/malloyyo 0.2.39 → 0.2.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.
@@ -35,10 +35,16 @@ const givensToUrl = (dashboard, givens) => {
35
35
 
36
36
  mountInPage({
37
37
  root: document.getElementById("root"),
38
- // Governed query — the shell's trusted /api/run (the same endpoint the iframe
38
+ // Governed query — the shell's trusted api/run (the same endpoint the iframe
39
39
  // broker forwards to). Returns the raw result the runtime normalizes.
40
+ //
41
+ // RELATIVE. A leading slash resolves against the ORIGIN, so behind anything
42
+ // serving this page under a prefix — code-server's `/proxy/<port>/`, a
43
+ // Codespaces forwarded port — the POST went to the proxy's own root and came
44
+ // back `405 Method Not Allowed` from the proxy rather than reaching the dev
45
+ // server at all. Resolved against the document, it is correct at any depth.
40
46
  run: (req, givens) =>
41
- fetch("/api/run", {
47
+ fetch("api/run", {
42
48
  method: "POST",
43
49
  headers: { "content-type": "application/json" },
44
50
  body: JSON.stringify({ d: name, query: req.query, malloy: req.malloy, givens }),
package/dist/index.js CHANGED
@@ -2881,7 +2881,7 @@ function clearCreds(url6) {
2881
2881
  }
2882
2882
 
2883
2883
  // package.json
2884
- var version = "0.2.39";
2884
+ var version = "0.2.41";
2885
2885
 
2886
2886
  // src/http.ts
2887
2887
  var USER_AGENT = `malloyyo/${version}`;
@@ -2928,15 +2928,34 @@ async function registerClient(registrationEndpoint, redirectUri) {
2928
2928
  if (!res.ok) throw new Error(`client registration failed: ${res.status} ${await res.text()}`);
2929
2929
  return (await res.json()).client_id;
2930
2930
  }
2931
+ function browserless(platform = process.platform, env = process.env) {
2932
+ if (platform === "darwin" || platform === "win32") return false;
2933
+ return !env.DISPLAY && !env.WAYLAND_DISPLAY;
2934
+ }
2931
2935
  function openBrowser(url6) {
2932
2936
  const [cmd, args] = process.platform === "darwin" ? ["open", [url6]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url6]] : ["xdg-open", [url6]];
2933
2937
  try {
2934
- spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
2938
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
2939
+ child.on("error", () => {
2940
+ });
2941
+ child.unref();
2935
2942
  } catch {
2936
2943
  }
2937
2944
  }
2945
+ function listenTarget(env = process.env) {
2946
+ const raw = env.MALLOYYO_OAUTH_PORT;
2947
+ let port = 0;
2948
+ if (raw !== void 0 && raw !== "") {
2949
+ port = Number(raw);
2950
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
2951
+ throw new Error(`MALLOYYO_OAUTH_PORT must be a port number between 1 and 65535, got "${raw}"`);
2952
+ }
2953
+ }
2954
+ return { host: env.MALLOYYO_OAUTH_HOST || "127.0.0.1", port };
2955
+ }
2938
2956
  function awaitRedirect(state) {
2939
- return new Promise((resolveServer) => {
2957
+ return new Promise((resolveServer, rejectServer) => {
2958
+ const { host, port: wanted } = listenTarget();
2940
2959
  let resolveCode;
2941
2960
  let rejectCode;
2942
2961
  const code = new Promise((res, rej) => {
@@ -2961,13 +2980,33 @@ function awaitRedirect(state) {
2961
2980
  if (ok) resolveCode(got);
2962
2981
  else rejectCode(new Error(err ?? "state mismatch or missing code"));
2963
2982
  });
2964
- server.listen(0, "127.0.0.1", () => {
2983
+ let listening = false;
2984
+ server.on("error", (err) => {
2985
+ clearTimeout(timer);
2986
+ const detail = err.code === "EADDRINUSE" ? `${host}:${wanted} is already in use \u2014 set MALLOYYO_OAUTH_PORT to a free port` : err.message;
2987
+ const failure = new Error(`could not start the sign-in listener: ${detail}`);
2988
+ rejectCode(failure);
2989
+ if (!listening) {
2990
+ void code.catch(() => {
2991
+ });
2992
+ rejectServer(failure);
2993
+ }
2994
+ });
2995
+ server.listen(wanted, host, () => {
2996
+ listening = true;
2965
2997
  const port = server.address().port;
2966
- resolveServer({ port, code, close: () => server.close() });
2998
+ resolveServer({
2999
+ port,
3000
+ code,
3001
+ close: () => {
3002
+ clearTimeout(timer);
3003
+ server.close();
3004
+ }
3005
+ });
2967
3006
  });
2968
3007
  });
2969
3008
  }
2970
- async function login(baseUrl) {
3009
+ async function login(baseUrl, opts = {}) {
2971
3010
  const ep = await discover(baseUrl);
2972
3011
  const { verifier, challenge } = pkce();
2973
3012
  const state = crypto.randomBytes(16).toString("base64url");
@@ -2985,11 +3024,24 @@ async function login(baseUrl) {
2985
3024
  scope: "mcp",
2986
3025
  state
2987
3026
  }).toString();
2988
- console.log("Opening your browser to sign in\u2026");
2989
- console.log(`If it doesn't open, visit:
3027
+ if (opts.noBrowser || browserless()) {
3028
+ console.log(`Visit this URL to sign in:
3029
+
2990
3030
  ${authUrl.toString()}
2991
3031
  `);
2992
- openBrowser(authUrl.toString());
3032
+ if (!process.env.MALLOYYO_OAUTH_PORT) {
3033
+ console.log(
3034
+ "Note: sign-in redirects back to this machine on a random port.\n In a container, set MALLOYYO_OAUTH_PORT and MALLOYYO_OAUTH_HOST=0.0.0.0,\n and publish that port, so the browser can reach the redirect.\n"
3035
+ );
3036
+ }
3037
+ console.log("Waiting for sign-in to complete\u2026");
3038
+ } else {
3039
+ console.log("Opening your browser to sign in\u2026");
3040
+ console.log(`If it doesn't open, visit:
3041
+ ${authUrl.toString()}
3042
+ `);
3043
+ openBrowser(authUrl.toString());
3044
+ }
2993
3045
  const authCode = await code;
2994
3046
  const res = await apiFetch(ep.token_endpoint, {
2995
3047
  method: "POST",
@@ -3330,6 +3382,7 @@ function siblingList(current, all, href) {
3330
3382
  // src/dashboard.ts
3331
3383
  var resolveFrameEntry = () => path6.join(resolveRuntimeDir(), "..", "frame-entry.tsx");
3332
3384
  var resolveInPageEntry = () => path6.join(resolveRuntimeDir(), "..", "frame-inpage-entry.tsx");
3385
+ var inlineScript = (js) => js.replace(/<\/(script)/gi, String.raw`<\/$1`);
3333
3386
  var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3334
3387
  function makeBundler() {
3335
3388
  const cache = /* @__PURE__ */ new Map();
@@ -3405,9 +3458,26 @@ function makeInPageBundler() {
3405
3458
  };
3406
3459
  }
3407
3460
  var html = (body, title) => `<!doctype html><html><head><meta charset="utf-8"><title>${esc2(title)}</title><meta name="viewport" content="width=device-width,initial-scale=1"><style>${NAV_CSS}</style></head><body style="margin:0">${body}</body></html>`;
3408
- var devSiblings = (dash, all) => siblingList(dash.name, all, (n) => `/?d=${encodeURIComponent(n)}`);
3461
+ var dashLink = (n) => `?d=${encodeURIComponent(n)}`;
3462
+ var DEV_PATHS = {
3463
+ /** Sibling dashboard, for the switcher in the nav bar. */
3464
+ dashboard: dashLink,
3465
+ /** In-page (tag-only) bundle. */
3466
+ inPage: (n) => `inpage.js?d=${encodeURIComponent(n)}`,
3467
+ /** Artifact bundle, for a dashboard with a Dashboard.tsx. */
3468
+ bundle: (n) => `bundle.js?d=${encodeURIComponent(n)}`,
3469
+ /** The sandboxed artifact document. Same origin as the shell, relative like
3470
+ everything else — the iframe is isolated by its OPAQUE origin (the
3471
+ `allow-scripts`-only sandbox), not by living on a second port. */
3472
+ frame: (n) => `frame?d=${encodeURIComponent(n)}`,
3473
+ /** Live-reload stream. */
3474
+ events: "events",
3475
+ /** The privileged query broker the parent shell brokers postMessage into. */
3476
+ run: "api/run"
3477
+ };
3478
+ var devSiblings = (dash, all) => siblingList(dash.name, all, dashLink);
3409
3479
  function navHtml2(dash, all) {
3410
- return navHtml(dash.name, all, (n) => `/?d=${encodeURIComponent(n)}`);
3480
+ return navHtml(dash.name, all, dashLink);
3411
3481
  }
3412
3482
  function inPageShell(dash, all, givenSpecs, initialGivens, initialUrlState, tileSpecs) {
3413
3483
  const info = {
@@ -3422,21 +3492,20 @@ function inPageShell(dash, all, givenSpecs, initialGivens, initialUrlState, tile
3422
3492
  autorun: dash.autorun
3423
3493
  };
3424
3494
  return html(
3425
- navHtml2(dash, all) + `<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__DASHBOARDS__=${safeJson(devSiblings(dash, all))};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script>try{new EventSource('/events').onmessage=()=>location.reload();}catch(e){}</script><script src="/inpage.js?d=${encodeURIComponent(dash.name)}"></script>`,
3495
+ navHtml2(dash, all) + `<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__DASHBOARDS__=${safeJson(devSiblings(dash, all))};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script>try{new EventSource('${DEV_PATHS.events}').onmessage=()=>location.reload();}catch(e){}</script><script src="${DEV_PATHS.inPage(dash.name)}"></script>`,
3426
3496
  dash.title
3427
3497
  );
3428
3498
  }
3429
- function parentShell(dash, frameBase, all, initialGivens, initialUrlState) {
3499
+ function parentShell(dash, all, initialGivens, initialUrlState) {
3430
3500
  const givensQs = Object.entries({ ...initialGivens, ...initialUrlState }).map(([k, v]) => `&${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("");
3431
3501
  const d = safeJson(dash.name);
3432
- const fb = safeJson(frameBase);
3433
3502
  const nav = navHtml2(dash, all);
3434
3503
  return html(
3435
3504
  `<div style="display:flex;flex-direction:column;height:100vh">` + nav + // allow-popups(+escape-sandbox): let a # link mark open its target in a
3436
3505
  // normal new tab on click instead of being blocked by the sandbox.
3437
- `<iframe id="f" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox" src="${frameBase}/frame?d=${encodeURIComponent(dash.name)}${givensQs}" style="border:0;flex:1;width:100%"></iframe></div><script>
3506
+ `<iframe id="f" sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox" src="${DEV_PATHS.frame(dash.name)}${givensQs}" style="border:0;flex:1;width:100%"></iframe></div><script>
3438
3507
  const f=document.getElementById('f');
3439
- try{new EventSource('/events').onmessage=()=>location.reload();}catch(e){}
3508
+ try{new EventSource('${DEV_PATHS.events}').onmessage=()=>location.reload();}catch(e){}
3440
3509
  // The shareable URL has TWO namespaces the frame syncs independently:
3441
3510
  // '$NAME' = a given (the governed query contract), '~key' = a custom
3442
3511
  // component's useUrlState view-state. Each write must re-emit the other's
@@ -3451,7 +3520,7 @@ function shareUrl(dashboard){
3451
3520
  return u.pathname+u.search;
3452
3521
  }
3453
3522
  window.addEventListener('message',async(e)=>{
3454
- if(e.source!==f.contentWindow||e.origin!==${fb})return;
3523
+ if(e.source!==f.contentWindow)return;
3455
3524
  const m=e.data;
3456
3525
  if(m&&m.type==='givens'){
3457
3526
  G=m.givens||{};
@@ -3473,11 +3542,11 @@ window.addEventListener('message',async(e)=>{
3473
3542
  if(!m||m.type!=='run')return;
3474
3543
  let out;
3475
3544
  try{
3476
- const res=await fetch('/api/run',{method:'POST',headers:{'content-type':'application/json'},
3545
+ const res=await fetch('${DEV_PATHS.run}',{method:'POST',headers:{'content-type':'application/json'},
3477
3546
  body:JSON.stringify({d:${d},query:m.query,malloy:m.malloy,givens:m.givens,dashboard:m.dashboard})});
3478
3547
  out=await res.json();
3479
3548
  }catch(err){ out={ok:false,problems:[{message:String(err)}]}; }
3480
- f.contentWindow.postMessage({type:'result',id:m.id,...out},${fb});
3549
+ f.contentWindow.postMessage({type:'result',id:m.id,...out},'*');
3481
3550
  });
3482
3551
  </script>`,
3483
3552
  dash.title
@@ -3489,7 +3558,7 @@ function givensFromUrl(url6) {
3489
3558
  function urlStateFromUrl(url6) {
3490
3559
  return urlStateFromSearch(url6.search);
3491
3560
  }
3492
- function frameDoc(dash, all, givenSpecs, initialGivens, initialUrlState, tileSpecs) {
3561
+ function frameDoc(dash, all, givenSpecs, initialGivens, initialUrlState, bundleJs, tileSpecs) {
3493
3562
  const info = {
3494
3563
  name: dash.name,
3495
3564
  query: dash.query,
@@ -3504,7 +3573,7 @@ function frameDoc(dash, all, givenSpecs, initialGivens, initialUrlState, tileSpe
3504
3573
  autorun: dash.autorun
3505
3574
  };
3506
3575
  return html(
3507
- `<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__DASHBOARDS__=${safeJson(devSiblings(dash, all))};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script src="/bundle.js?d=${encodeURIComponent(dash.name)}"></script>`,
3576
+ `<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__DASHBOARDS__=${safeJson(devSiblings(dash, all))};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script>${inlineScript(bundleJs)}</script>`,
3508
3577
  dash.title
3509
3578
  );
3510
3579
  }
@@ -3517,8 +3586,6 @@ async function serveDashboard(opts) {
3517
3586
  await initConnections();
3518
3587
  const root = path6.resolve(opts.root ?? process.cwd());
3519
3588
  const port = opts.port ?? 4173;
3520
- const framePort = port + 1;
3521
- const frameBase = `http://localhost:${framePort}`;
3522
3589
  const runner = await makeRunner(root);
3523
3590
  if (!runner.entryExists()) {
3524
3591
  throw new Error(`No index.malloy at ${root} \u2014 run this from a Malloy model repo.`);
@@ -3566,34 +3633,39 @@ async function serveDashboard(opts) {
3566
3633
  console.error(` (file watch unavailable: ${e.message} \u2014 edits won't auto-reload)`);
3567
3634
  }
3568
3635
  const handler = async (req, res) => {
3569
- const onFramePort = (req.socket.localPort ?? port) === framePort;
3570
- const url6 = new URL(req.url ?? "/", `http://localhost:${onFramePort ? framePort : port}`);
3636
+ const url6 = new URL(req.url ?? "/", `http://localhost:${port}`);
3571
3637
  const send = (code, type, body, extra = {}) => {
3572
3638
  res.writeHead(code, { "content-type": type, ...extra });
3573
3639
  res.end(body);
3574
3640
  };
3575
3641
  try {
3576
- if (onFramePort) {
3577
- if (url6.pathname === "/frame") {
3578
- const dash = pick(url6);
3579
- const g = await resolveGivens(dash);
3580
- if (!g.ok) {
3581
- return send(
3582
- 200,
3583
- "text/html; charset=utf-8",
3584
- html(`<pre style="color:crimson;padding:16px">model error: ${esc2(g.error)}</pre>`, dash.title)
3585
- );
3586
- }
3642
+ if (url6.pathname === "/frame") {
3643
+ const dash = pick(url6);
3644
+ const g = await resolveGivens(dash);
3645
+ if (!g.ok) {
3587
3646
  return send(
3588
3647
  200,
3589
3648
  "text/html; charset=utf-8",
3590
- frameDoc(dash, dashboards, g.union, givensFromUrl(url6), urlStateFromUrl(url6), g.tiles)
3649
+ html(`<pre style="color:crimson;padding:16px">model error: ${esc2(g.error)}</pre>`, dash.title)
3591
3650
  );
3592
3651
  }
3593
- if (url6.pathname === "/bundle.js") {
3594
- return send(200, "application/javascript; charset=utf-8", await bundle(pick(url6)));
3595
- }
3596
- return send(404, "text/plain", "not found");
3652
+ return send(
3653
+ 200,
3654
+ "text/html; charset=utf-8",
3655
+ frameDoc(
3656
+ dash,
3657
+ dashboards,
3658
+ g.union,
3659
+ givensFromUrl(url6),
3660
+ urlStateFromUrl(url6),
3661
+ await bundle(dash),
3662
+ g.tiles
3663
+ ),
3664
+ { "cache-control": "no-store" }
3665
+ );
3666
+ }
3667
+ if (url6.pathname === "/bundle.js") {
3668
+ return send(200, "application/javascript; charset=utf-8", await bundle(pick(url6)));
3597
3669
  }
3598
3670
  if (url6.pathname === "/events") {
3599
3671
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
@@ -3616,17 +3688,24 @@ async function serveDashboard(opts) {
3616
3688
  return send(
3617
3689
  200,
3618
3690
  "text/html; charset=utf-8",
3619
- inPageShell(dash, dashboards, g.union, givensFromUrl(url6), urlStateFromUrl(url6), g.tiles)
3691
+ inPageShell(dash, dashboards, g.union, givensFromUrl(url6), urlStateFromUrl(url6), g.tiles),
3692
+ { "cache-control": "no-store" }
3620
3693
  );
3621
3694
  }
3622
3695
  return send(
3623
3696
  200,
3624
3697
  "text/html; charset=utf-8",
3625
- parentShell(dash, frameBase, dashboards, givensFromUrl(url6), urlStateFromUrl(url6))
3698
+ parentShell(dash, dashboards, givensFromUrl(url6), urlStateFromUrl(url6)),
3699
+ { "cache-control": "no-store" }
3626
3700
  );
3627
3701
  }
3628
3702
  if (url6.pathname === "/inpage.js") {
3629
- return send(200, "application/javascript; charset=utf-8", await inPageBundle());
3703
+ return send(
3704
+ 200,
3705
+ "application/javascript; charset=utf-8",
3706
+ await inPageBundle(),
3707
+ { "cache-control": "no-store" }
3708
+ );
3630
3709
  }
3631
3710
  if (url6.pathname === "/api/run" && req.method === "POST") {
3632
3711
  const { d, query, malloy, givens } = JSON.parse(await readBody(req));
@@ -3642,12 +3721,10 @@ async function serveDashboard(opts) {
3642
3721
  }
3643
3722
  };
3644
3723
  const shellServer = http2.createServer(handler);
3645
- const frameServer = http2.createServer(handler);
3646
3724
  await new Promise((r) => shellServer.listen(port, r));
3647
- await new Promise((r) => frameServer.listen(framePort, r));
3648
3725
  console.error(`
3649
3726
  malloyyo dashboard dev \u2014 model: ${root}`);
3650
- console.error(` http://localhost:${port}/ (artifact origin: ${frameBase})`);
3727
+ console.error(` http://localhost:${port}/`);
3651
3728
  for (const d of dashboards) {
3652
3729
  const kind = d.tsxPath ? "custom (iframe)" : "tag-only (in-page)";
3653
3730
  console.error(` \u2022 ${d.name} (${kind}) \u2192 http://localhost:${port}/?d=${d.name}`);
@@ -5389,9 +5466,9 @@ async function status(target, opts) {
5389
5466
  console.log(` version ${s.version ?? "?"}` + (git?.sha ? ` ${git.branch}@${shortSha(git.sha)}` : ""));
5390
5467
  console.log(` ${s.compileError ? `\u2717 ${s.compileError}` : `\u2713 compiled ${s.compiledAt ?? ""}`}`);
5391
5468
  }
5392
- async function loginCmd(target) {
5469
+ async function loginCmd(target, opts) {
5393
5470
  const inst = resolveInstance(resolve3("."), target);
5394
- await login(inst.url);
5471
+ await login(inst.url, { noBrowser: opts.browser === false });
5395
5472
  console.log(`\u2713 logged in to ${inst.name} (${inst.url})`);
5396
5473
  }
5397
5474
  async function logoutCmd(target) {
@@ -5400,7 +5477,7 @@ async function logoutCmd(target) {
5400
5477
  }
5401
5478
  var program = new Command();
5402
5479
  program.name("malloyyo").description("Publish Malloy models to a Malloyyo instance").version(version);
5403
- program.command("login").argument("[target]", "target name or instance URL (optional if the config has one target)").description("sign in to an instance in your browser (stores a token)").action(loginCmd);
5480
+ program.command("login").argument("[target]", "target name or instance URL (optional if the config has one target)").option("--no-browser", "print the sign-in URL instead of launching a browser").description("sign in to an instance in your browser (stores a token)").action(loginCmd);
5404
5481
  program.command("logout").argument("[target]", "target name or instance URL (optional if the config has one target)").description("forget the stored token for an instance").action(logoutCmd);
5405
5482
  program.command("publish").argument(
5406
5483
  "[target]",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloyyo",
3
- "version": "0.2.39",
3
+ "version": "0.2.41",
4
4
  "description": "Publish Malloy models to a Malloyyo instance",
5
5
  "license": "MIT",
6
6
  "repository": {