@segmentstream-cloud/app 0.16.2 → 0.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/neon.js CHANGED
@@ -8,19 +8,14 @@ import { NOT_INVITED, SIGN_IN_LINK_INVALID, SegmentStreamApiError, apiErrorFrom,
8
8
  import { parseMe } from "./me.js";
9
9
  import { createOntologyTransport } from "./ontology-transport.js";
10
10
  import { isAllowed } from "./permissions.js";
11
- import { matchesPathPrefix, normalizePublicPath } from "./proxy.js";
11
+ import { matchesPathPrefix, normalizePublicPath, wellKnownResponse } from "./proxy.js";
12
12
  import { isApiRequest, unauthenticatedResponse } from "./sign-in.js";
13
13
  /** The app's sign-in page, which renders `<SignIn />` from `@segmentstream-cloud/app/ui`. */
14
14
  export const SIGN_IN_PAGE = "/sign-in";
15
15
  /** Where `app/src/app/api/auth/[...path]/route.ts` serves the Neon Auth API. */
16
16
  export const AUTH_API_PATH = "/api/auth";
17
- /** The paths the ontology service answers: its HTTP API, its MCP server and its OAuth metadata. */
18
- const ONTOLOGY_PATHS = [
19
- "/api/ontology",
20
- "/mcp",
21
- "/.well-known/oauth-protected-resource",
22
- "/.well-known/oauth-authorization-server",
23
- ];
17
+ /** The ontology's HTTP API and MCP transport; metadata probes are handled separately. */
18
+ const ONTOLOGY_PATHS = ["/api/ontology", "/mcp"];
24
19
  /**
25
20
  * Paths every visitor may open: the Neon Auth API, the sign-in page, the health check, and the
26
21
  * Next.js assets and metadata files the sign-in page needs.
@@ -310,6 +305,10 @@ export function createNeonSession(dependencies) {
310
305
  const publicPaths = [...OPEN_PATHS, ...(options.publicPaths ?? [])].map(normalizePublicPath);
311
306
  return async function neonProxy(request) {
312
307
  const { pathname, search } = request.nextUrl;
308
+ const discovery = wellKnownResponse(request, settings.ontologyUrl);
309
+ if (discovery !== undefined) {
310
+ return discovery;
311
+ }
313
312
  if (settings.ontologyUrl !== undefined &&
314
313
  ONTOLOGY_PATHS.some((path) => matchesPathPrefix(pathname, path))) {
315
314
  // Local development: the service runs elsewhere; deployed, its paths never reach the app.
@@ -8,6 +8,52 @@ function escape(value) {
8
8
  "'": "&#39;",
9
9
  })[character]);
10
10
  }
11
+ // The shadcn/ui tokens and Card, Input and Button styles of the workspace app template, inlined so
12
+ // the page needs no external stylesheet under its strict CSP.
13
+ const STYLE = `
14
+ :root{color-scheme:light;--radius:0.625rem;--background:oklch(1 0 0);--foreground:oklch(0.145 0 0);
15
+ --card:oklch(1 0 0);--primary:oklch(0.205 0 0);--primary-foreground:oklch(0.985 0 0);
16
+ --muted:oklch(0.97 0 0);--muted-foreground:oklch(0.556 0 0);--accent:oklch(0.97 0 0);
17
+ --border:oklch(0.922 0 0);--input:oklch(0.922 0 0);--ring:oklch(0.708 0 0);--field:transparent;
18
+ --outline:var(--background);--outline-hover:var(--accent);--logo:none}
19
+ @media (prefers-color-scheme:dark){:root{color-scheme:dark;--background:oklch(0.145 0 0);
20
+ --foreground:oklch(0.985 0 0);--card:oklch(0.205 0 0);--primary:oklch(0.922 0 0);
21
+ --primary-foreground:oklch(0.205 0 0);--muted:oklch(0.269 0 0);--muted-foreground:oklch(0.708 0 0);
22
+ --accent:oklch(0.269 0 0);--border:oklch(1 0 0 / 10%);--input:oklch(1 0 0 / 15%);
23
+ --ring:oklch(0.556 0 0);--field:oklch(1 0 0 / 4.5%);--outline:oklch(1 0 0 / 4.5%);
24
+ --outline-hover:oklch(1 0 0 / 7.5%);--logo:invert(1)}}
25
+ *{box-sizing:border-box}
26
+ body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
27
+ color:var(--foreground);background:color-mix(in oklab,var(--muted) 40%,var(--background));
28
+ -webkit-font-smoothing:antialiased}
29
+ main{min-height:100svh;max-width:calc(28rem + 3rem);margin:0 auto;padding:1.5rem;display:flex;
30
+ flex-direction:column;align-items:center;justify-content:center;gap:1.5rem}
31
+ main>img{filter:var(--logo);height:auto;max-width:100%}
32
+ .card{width:100%;display:flex;flex-direction:column;gap:1.5rem;padding:1.5rem;
33
+ background:var(--card);border:1px solid var(--border);border-radius:calc(var(--radius) + 4px);
34
+ box-shadow:0 1px 3px 0 rgb(0 0 0 / 0.1),0 1px 2px -1px rgb(0 0 0 / 0.1)}
35
+ header,.content{display:grid;gap:0.5rem}
36
+ h1{margin:0;font-size:1.25rem;line-height:1.75rem;font-weight:600;letter-spacing:-0.025em}
37
+ p{margin:0}.muted{color:var(--muted-foreground);font-size:0.875rem;line-height:1.25rem}
38
+ .status:empty{display:none}.secured{font-size:0.75rem;line-height:1rem;text-align:center}
39
+ .field{display:flex;gap:0.5rem}
40
+ input{flex:1;min-width:0;height:2.25rem;padding:0 0.75rem;border:1px solid var(--input);
41
+ border-radius:calc(var(--radius) - 2px);background:var(--field);color:inherit;
42
+ font:0.875rem/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:0.025em;
43
+ box-shadow:0 1px 2px 0 rgb(0 0 0 / 0.05);outline:none}
44
+ .button{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;height:2.25rem;
45
+ padding:0 1rem;border:1px solid transparent;border-radius:calc(var(--radius) - 2px);
46
+ background:var(--primary);color:var(--primary-foreground);font-family:inherit;font-size:0.875rem;
47
+ font-weight:500;line-height:1.25rem;text-decoration:none;white-space:nowrap;cursor:pointer;outline:none;
48
+ transition:background-color .15s,box-shadow .15s}
49
+ .button:hover{background:color-mix(in oklab,var(--primary) 90%,transparent)}
50
+ .outline{background:var(--outline);color:var(--foreground);border-color:var(--input);
51
+ box-shadow:0 1px 2px 0 rgb(0 0 0 / 0.05)}.outline:hover{background:var(--outline-hover)}
52
+ input:focus-visible,.button:focus-visible{border-color:var(--ring);
53
+ box-shadow:0 0 0 3px color-mix(in oklab,var(--ring) 50%,transparent)}
54
+ footer{display:flex;justify-content:flex-end}
55
+ @media (max-width:30rem){.field{flex-direction:column}input{flex:none}.button{width:100%}}
56
+ `;
11
57
  /** Keep a copyable PKCE-bound code visible while trying the client's loopback callback. */
12
58
  export function oauthCompletion(options) {
13
59
  // Web Crypto keeps this module bundleable for client components (no node: imports).
@@ -16,12 +62,14 @@ export function oauthCompletion(options) {
16
62
  const code = redirect === undefined ? null : new URL(redirect).searchParams.get("code");
17
63
  const title = options.approved === false ? "Request denied" : "Authorization complete";
18
64
  const manual = code === null
19
- ? "<p>You can return to your terminal.</p>"
20
- : `
21
- <p>Return to your terminal. If it is still waiting, copy and paste this code:</p>
65
+ ? `<header><h1>${title}</h1><p class="muted">You can return to your terminal.</p></header>`
66
+ : `<header><h1>${title}</h1>
67
+ <p class="muted">Return to your terminal. If it is still waiting, copy and paste this code:</p></header>
68
+ <div class="content"><div class="field">
22
69
  <input id="code" aria-label="Authorization code" readonly value="${escape(code)}">
23
- <button id="copy" type="button">Copy code</button><p id="status" role="status"></p>
24
- <p><a href="${escape(redirect)}">Continue to the client</a></p>`;
70
+ <button id="copy" class="button" type="button">Copy code</button></div>
71
+ <p id="status" class="muted status" role="status"></p></div>
72
+ <footer><a class="button outline" href="${escape(redirect)}">Continue to the client</a></footer>`;
25
73
  const script = code === null
26
74
  ? ""
27
75
  : `
@@ -36,10 +84,11 @@ export function oauthCompletion(options) {
36
84
  frame.src = ${JSON.stringify(redirect).replace(/</g, "\\u003c")}; document.body.appendChild(frame);`;
37
85
  return new Response(`<!doctype html><html lang="en"><meta charset="utf-8">
38
86
  <meta name="viewport" content="width=device-width, initial-scale=1"><title>${title}</title>
39
- <style nonce="${nonce}">body{font:16px system-ui;background:#fafafa;color:#18181b;margin:10vh auto;
40
- max-width:480px;padding:24px}input,button{font:inherit;padding:12px;margin:8px 0}input{width:95%}</style>
41
- <img src="${escape(SEGMENTSTREAM_LOGO_URI)}" alt="SegmentStream" width="210" height="45">
42
- <h1>${title}</h1>${manual}<script nonce="${nonce}">${script}</script></html>`, {
87
+ <style nonce="${nonce}">${STYLE}</style>
88
+ <main><img src="${escape(SEGMENTSTREAM_LOGO_URI)}" alt="SegmentStream" width="210" height="45">
89
+ <section class="card">${manual}</section>
90
+ <p class="muted secured">Secured by SegmentStream</p></main>
91
+ <script nonce="${nonce}">${script}</script></html>`, {
43
92
  headers: {
44
93
  "content-type": "text/html; charset=utf-8",
45
94
  "cache-control": "no-store",
package/dist/proxy.d.ts CHANGED
@@ -11,5 +11,7 @@ export interface ProxyOptions {
11
11
  }
12
12
  export type SegmentStreamProxy = (request: NextRequest) => Promise<NextResponse>;
13
13
  export declare function createProxy(context: AppContext, options?: ProxyOptions): SegmentStreamProxy;
14
+ /** Discovery probes never start browser authentication, including probes for unsupported metadata. */
15
+ export declare function wellKnownResponse(request: NextRequest, ontologyUrl?: string): NextResponse | undefined;
14
16
  export declare function normalizePublicPath(path: string): string;
15
17
  export declare function matchesPathPrefix(pathname: string, prefix: string): boolean;
package/dist/proxy.js CHANGED
@@ -22,6 +22,10 @@ export function createProxy(context, options = {}) {
22
22
  }
23
23
  return async function proxy(request) {
24
24
  const pathname = request.nextUrl.pathname;
25
+ const discovery = wellKnownResponse(request);
26
+ if (discovery !== undefined) {
27
+ return discovery;
28
+ }
25
29
  if (matchesPathPrefix(pathname, SDK_ROUTE_PREFIX) ||
26
30
  publicPaths.some((path) => matchesPathPrefix(pathname, path))) {
27
31
  return NextResponse.next();
@@ -58,6 +62,26 @@ export function createProxy(context, options = {}) {
58
62
  return startSignIn(context, request, policy, true);
59
63
  };
60
64
  }
65
+ const ONTOLOGY_METADATA_PATHS = new Set([
66
+ "/.well-known/oauth-protected-resource",
67
+ "/.well-known/oauth-protected-resource/mcp",
68
+ "/.well-known/oauth-authorization-server",
69
+ ]);
70
+ /** Discovery probes never start browser authentication, including probes for unsupported metadata. */
71
+ export function wellKnownResponse(request, ontologyUrl) {
72
+ const { pathname, search } = request.nextUrl;
73
+ if (!matchesPathPrefix(pathname, "/.well-known")) {
74
+ return undefined;
75
+ }
76
+ if (ONTOLOGY_METADATA_PATHS.has(pathname)) {
77
+ return ontologyUrl === undefined
78
+ ? NextResponse.next()
79
+ : NextResponse.rewrite(new URL(`${pathname}${search}`, ontologyUrl));
80
+ }
81
+ // The issuer is the root origin, so RFC 8414 does not define an AS document suffixed by /mcp.
82
+ // This service also does not publish OpenID Connect discovery metadata.
83
+ return NextResponse.json({ error: "not_found" }, { status: 404 });
84
+ }
61
85
  /**
62
86
  * SegmentStream is unreachable. API requests get a retryable 503; pages restart sign-in,
63
87
  * which ends on the callback's error page rather than looping. Session cookies are kept.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream-cloud/app",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
4
4
  "description": "Sign-in, users, permissions, the ontology client and warehouse data access for SegmentStream workspace apps built with Next.js.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SegmentStream",