@segmentstream-cloud/app 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -8
- package/dist/agent.d.ts +2 -1
- package/dist/agent.js +1 -0
- package/dist/app.js +84 -15
- package/dist/channel-protocol.d.ts +16 -0
- package/dist/channel-protocol.js +17 -0
- package/dist/channel-session.d.ts +22 -0
- package/dist/channel-session.js +89 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +11 -1
- package/dist/decision-pages.d.ts +2 -1
- package/dist/decision-pages.js +15 -2
- package/dist/federation.d.ts +6 -0
- package/dist/federation.js +114 -36
- package/dist/handlers.d.ts +3 -1
- package/dist/handlers.js +5 -1
- package/dist/neon.js +43 -3
- package/dist/ontology-transport.js +15 -5
- package/dist/proxy.js +8 -4
- package/dist/telegram-launch.d.ts +6 -0
- package/dist/telegram-launch.js +98 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +6 -0
- package/dist/ui/components.d.ts +1 -1
- package/dist/ui/components.js +10 -3
- package/dist/ui/hooks.js +2 -2
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/sign-in.d.ts +5 -1
- package/dist/ui/sign-in.js +15 -1
- package/dist/ui/store.d.ts +15 -1
- package/dist/ui/store.js +145 -25
- package/dist/ui/telegram-sign-in.d.ts +16 -0
- package/dist/ui/telegram-sign-in.js +21 -0
- package/dist/ui/use-channel-sign-in.d.ts +10 -0
- package/dist/ui/use-channel-sign-in.js +118 -0
- package/dist/ui/use-sign-in.js +10 -2
- package/package.json +5 -5
package/dist/federation.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { cookies, headers } from "next/headers";
|
|
2
2
|
import { redirect } from "next/navigation";
|
|
3
3
|
import { NextResponse } from "next/server";
|
|
4
|
+
import { hasChannelApps } from "./config.js";
|
|
4
5
|
import { cookiePolicyFor, isLoopbackHostname, safeReturnTo } from "./cookies.js";
|
|
5
|
-
import {
|
|
6
|
+
import { channelAppFraming } from "./telegram.js";
|
|
7
|
+
import { createChannelSession } from "./channel-session.js";
|
|
8
|
+
import { isSameOrigin, withUnframeablePages } from "./decision-pages.js";
|
|
9
|
+
import { apiErrorFrom, SegmentStreamApiError } from "./errors.js";
|
|
6
10
|
import { parseMe } from "./me.js";
|
|
7
11
|
import { createOntologyTransport, ontologyOrigin } from "./ontology-transport.js";
|
|
8
12
|
import { createOntologyClient } from "./ontology.js";
|
|
9
13
|
import { createAgentAuthorizations } from "./oauth.js";
|
|
10
14
|
import { createAgentIdentities } from "./agent.js";
|
|
11
15
|
import { isAllowed } from "./permissions.js";
|
|
16
|
+
import { matchesPathPrefix, normalizePublicPath } from "./proxy.js";
|
|
12
17
|
import { base64UrlDecode, base64UrlEncode, constantTimeEqual, createCodeChallenge, randomToken, } from "./pkce.js";
|
|
13
18
|
const PREFIX = "/api/segmentstream";
|
|
14
19
|
const CALLBACK = `${PREFIX}/callback`;
|
|
@@ -34,6 +39,12 @@ export function withFederation(base, config, oidc, options) {
|
|
|
34
39
|
oidc.issuer.endsWith("/")) {
|
|
35
40
|
throw new Error("SEGMENTSTREAM_OIDC_ISSUER must be an HTTPS issuer without a trailing slash.");
|
|
36
41
|
}
|
|
42
|
+
const channel = createChannelSession({
|
|
43
|
+
enabled: hasChannelApps(config.channels),
|
|
44
|
+
workspaceId: config.workspaceId,
|
|
45
|
+
ontologyUrl: options.ontologyUrl,
|
|
46
|
+
fetch: options.fetch,
|
|
47
|
+
});
|
|
37
48
|
const token = async () => {
|
|
38
49
|
const cookieStore = await cookies();
|
|
39
50
|
const host = (await headers()).get("host") ?? "";
|
|
@@ -50,32 +61,71 @@ export function withFederation(base, config, oidc, options) {
|
|
|
50
61
|
ontologyUrl: options.ontologyUrl,
|
|
51
62
|
credentials: { token },
|
|
52
63
|
});
|
|
64
|
+
const fallbackTransport = options.proofTransport ?? options.channelSession?.transport ?? transport;
|
|
53
65
|
const ontologyTransport = {
|
|
54
|
-
fetch: async (path, init = {}) => (await
|
|
55
|
-
? transport.fetch(path, init)
|
|
56
|
-
:
|
|
57
|
-
?
|
|
58
|
-
:
|
|
66
|
+
fetch: async (path, init = {}) => (await channel.read())
|
|
67
|
+
? channel.transport.fetch(path, init)
|
|
68
|
+
: (await token())
|
|
69
|
+
? transport.fetch(path, init)
|
|
70
|
+
: fallbackTransport.fetch(path, init),
|
|
59
71
|
};
|
|
60
72
|
async function getUser() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
? {
|
|
68
|
-
id: user.id,
|
|
69
|
-
...(user.email ? { email: user.email } : {}),
|
|
70
|
-
...(user.name ? { name: user.name } : {}),
|
|
73
|
+
const channelUser = (await channel.read())?.user;
|
|
74
|
+
if (channelUser) {
|
|
75
|
+
return {
|
|
76
|
+
...channelUser,
|
|
77
|
+
email: channelUser.email ?? undefined,
|
|
78
|
+
name: channelUser.name ?? undefined,
|
|
71
79
|
organizationId: config.organizationId,
|
|
72
80
|
workspaceId: config.workspaceId,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (!(await token())) {
|
|
84
|
+
return options.neon ? base.getUser() : (options.channelSession?.getUser() ?? null);
|
|
85
|
+
}
|
|
86
|
+
return oidcUser();
|
|
87
|
+
}
|
|
88
|
+
async function oidcUser() {
|
|
89
|
+
return userFromResponse(await transport.fetch("/me", { cache: "no-store" }));
|
|
90
|
+
}
|
|
91
|
+
async function userFromResponse(response) {
|
|
92
|
+
if (response.status === 401 || response.status === 403) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
let payload;
|
|
96
|
+
try {
|
|
97
|
+
payload = await response.json();
|
|
98
|
+
}
|
|
99
|
+
catch (cause) {
|
|
100
|
+
throw new SegmentStreamApiError({
|
|
101
|
+
code: "ontology_invalid_response",
|
|
102
|
+
status: 502,
|
|
103
|
+
detail: "ontology_invalid_response",
|
|
104
|
+
cause,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw apiErrorFrom(response.status, payload, "session_unavailable");
|
|
109
|
+
}
|
|
110
|
+
const user = parseMe(payload);
|
|
111
|
+
if (!user) {
|
|
112
|
+
throw new SegmentStreamApiError({
|
|
113
|
+
code: "ontology_invalid_response",
|
|
114
|
+
status: 502,
|
|
115
|
+
detail: "ontology_invalid_response",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
id: user.id,
|
|
120
|
+
...(user.email ? { email: user.email } : {}),
|
|
121
|
+
...(user.name ? { name: user.name } : {}),
|
|
122
|
+
organizationId: config.organizationId,
|
|
123
|
+
workspaceId: config.workspaceId,
|
|
124
|
+
roles: user.roles,
|
|
125
|
+
permissions: user.permissions,
|
|
126
|
+
kind: user.kind,
|
|
127
|
+
active: user.active,
|
|
128
|
+
};
|
|
79
129
|
}
|
|
80
130
|
const setSession = (response, request, value) => {
|
|
81
131
|
const { secure } = cookiePolicyFor(new URL(request.url), config.workspaceId);
|
|
@@ -87,7 +137,24 @@ export function withFederation(base, config, oidc, options) {
|
|
|
87
137
|
const pendingName = cookieName(config.workspaceId, secure, "pending");
|
|
88
138
|
const path = request.nextUrl.pathname;
|
|
89
139
|
if (path === `${PREFIX}/configuration`) {
|
|
90
|
-
return NextResponse.json({ enabled: true }, { headers: { "cache-control": "no-store" } });
|
|
140
|
+
return NextResponse.json({ enabled: true, neon: options.neon, channels: config.channels ?? {} }, { headers: { "cache-control": "no-store" } });
|
|
141
|
+
}
|
|
142
|
+
if (path === `${PREFIX}/session`) {
|
|
143
|
+
const credential = await token();
|
|
144
|
+
if (!credential) {
|
|
145
|
+
return NextResponse.json(null, { status: 401, headers: { "cache-control": "no-store" } });
|
|
146
|
+
}
|
|
147
|
+
const user = await oidcUser();
|
|
148
|
+
return NextResponse.json(user
|
|
149
|
+
? {
|
|
150
|
+
session: { id: await createCodeChallenge(credential) },
|
|
151
|
+
user,
|
|
152
|
+
source: "oidc",
|
|
153
|
+
}
|
|
154
|
+
: { error: "unauthorized", source: "oidc" }, {
|
|
155
|
+
status: user ? 200 : 401,
|
|
156
|
+
headers: { "cache-control": "no-store" },
|
|
157
|
+
});
|
|
91
158
|
}
|
|
92
159
|
if (path === `${PREFIX}/sign-in`) {
|
|
93
160
|
const state = randomToken();
|
|
@@ -261,6 +328,7 @@ export function withFederation(base, config, oidc, options) {
|
|
|
261
328
|
oauth: createAgentAuthorizations(ontologyTransport),
|
|
262
329
|
agent: createAgentIdentities(ontologyTransport),
|
|
263
330
|
signOut: async (settings) => {
|
|
331
|
+
await options.channelSession?.signOut();
|
|
264
332
|
if (await token()) {
|
|
265
333
|
await transport.fetch("/federation/logout", { method: "POST" }).catch(() => undefined);
|
|
266
334
|
}
|
|
@@ -273,22 +341,28 @@ export function withFederation(base, config, oidc, options) {
|
|
|
273
341
|
}
|
|
274
342
|
redirect(safeReturnTo(settings?.redirectTo ?? "/sign-in"));
|
|
275
343
|
},
|
|
276
|
-
proxy: (settings) => async (request) => {
|
|
344
|
+
proxy: (settings) => withUnframeablePages(async (request) => {
|
|
345
|
+
const forwardedOrPublic = [
|
|
346
|
+
PREFIX,
|
|
347
|
+
"/sign-in",
|
|
348
|
+
"/api/auth",
|
|
349
|
+
"/mcp",
|
|
350
|
+
"/api/ontology",
|
|
351
|
+
"/_next",
|
|
352
|
+
"/.well-known",
|
|
353
|
+
...(settings?.publicPaths ?? []).map(normalizePublicPath),
|
|
354
|
+
].some((path) => matchesPathPrefix(request.nextUrl.pathname, path));
|
|
355
|
+
const channelResponse = forwardedOrPublic ? undefined : await channel.proxy(request);
|
|
356
|
+
if (channelResponse) {
|
|
357
|
+
return channelResponse;
|
|
358
|
+
}
|
|
277
359
|
const { secure } = cookiePolicyFor(new URL(request.url), config.workspaceId);
|
|
278
360
|
const credential = request.cookies.get(cookieName(config.workspaceId, secure))?.value;
|
|
279
|
-
if (request.nextUrl.pathname
|
|
361
|
+
if (matchesPathPrefix(request.nextUrl.pathname, PREFIX)) {
|
|
280
362
|
return NextResponse.next();
|
|
281
363
|
}
|
|
282
|
-
if (credential &&
|
|
283
|
-
|
|
284
|
-
"/sign-in",
|
|
285
|
-
"/api/auth",
|
|
286
|
-
"/mcp",
|
|
287
|
-
"/api/ontology",
|
|
288
|
-
"/_next",
|
|
289
|
-
...(settings?.publicPaths ?? []),
|
|
290
|
-
].some((path) => request.nextUrl.pathname === path || request.nextUrl.pathname.startsWith(`${path}/`))) {
|
|
291
|
-
const origin = options.ontologyUrl ?? request.nextUrl.origin;
|
|
364
|
+
if (credential && !forwardedOrPublic) {
|
|
365
|
+
const origin = await ontologyOrigin(options.ontologyUrl ?? request.nextUrl.origin);
|
|
292
366
|
const response = await options.fetch(`${origin}/api/ontology/me`, {
|
|
293
367
|
headers: { authorization: `Bearer ${credential}` },
|
|
294
368
|
cache: "no-store",
|
|
@@ -296,14 +370,18 @@ export function withFederation(base, config, oidc, options) {
|
|
|
296
370
|
signal: AbortSignal.timeout(10000),
|
|
297
371
|
});
|
|
298
372
|
if (response.ok) {
|
|
373
|
+
await userFromResponse(response);
|
|
299
374
|
return NextResponse.next();
|
|
300
375
|
}
|
|
376
|
+
if (response.status !== 401 && response.status !== 403) {
|
|
377
|
+
return NextResponse.json({ error: "session_unavailable" }, { status: 503, headers: { "cache-control": "no-store", "retry-after": "5" } });
|
|
378
|
+
}
|
|
301
379
|
const denied = NextResponse.redirect(new URL("/sign-in", request.url));
|
|
302
380
|
denied.cookies.set(cookieName(config.workspaceId, secure), "", attributes(secure, 0));
|
|
303
381
|
return denied;
|
|
304
382
|
}
|
|
305
383
|
return base.proxy(settings)(request);
|
|
306
|
-
},
|
|
384
|
+
}, channelAppFraming(config.channels)),
|
|
307
385
|
};
|
|
308
386
|
}
|
|
309
387
|
function protectResponse(response) {
|
package/dist/handlers.d.ts
CHANGED
|
@@ -5,4 +5,6 @@ export interface SegmentStreamRouteHandlers {
|
|
|
5
5
|
readonly GET: RouteHandler;
|
|
6
6
|
readonly POST: RouteHandler;
|
|
7
7
|
}
|
|
8
|
-
export declare function createHandlers(context: AppContext
|
|
8
|
+
export declare function createHandlers(context: AppContext, { neon }?: {
|
|
9
|
+
readonly neon?: boolean;
|
|
10
|
+
}): SegmentStreamRouteHandlers;
|
package/dist/handlers.js
CHANGED
|
@@ -3,9 +3,13 @@ import { authFlowCookieName, clearCookie, clearSessionCookies, cookiePolicyFor,
|
|
|
3
3
|
import { isSameOrigin } from "./decision-pages.js";
|
|
4
4
|
import { constantTimeEqual } from "./pkce.js";
|
|
5
5
|
import { CALLBACK_PATH, SIGNED_OUT_PATH, SIGN_IN_PATH, SDK_ROUTE_PREFIX, signInRedirect, } from "./sign-in.js";
|
|
6
|
-
export function createHandlers(context) {
|
|
6
|
+
export function createHandlers(context, { neon = false } = {}) {
|
|
7
7
|
async function GET(request) {
|
|
8
8
|
switch (request.nextUrl.pathname) {
|
|
9
|
+
case `${SDK_ROUTE_PREFIX}/configuration`:
|
|
10
|
+
return NextResponse.json({ enabled: false, neon, channels: context.config.channels ?? {} }, {
|
|
11
|
+
headers: { "cache-control": "no-store" },
|
|
12
|
+
});
|
|
9
13
|
case CALLBACK_PATH:
|
|
10
14
|
return handleCallback(context, request);
|
|
11
15
|
case SIGN_IN_PATH:
|
package/dist/neon.js
CHANGED
|
@@ -3,7 +3,9 @@ import { decodeJwt } from "jose";
|
|
|
3
3
|
import { cookies } from "next/headers";
|
|
4
4
|
import { redirect } from "next/navigation";
|
|
5
5
|
import { NextResponse } from "next/server";
|
|
6
|
+
import { hasChannelApps } from "./config.js";
|
|
6
7
|
import { replaceRequestCookies, safeReturnTo } from "./cookies.js";
|
|
8
|
+
import { createChannelSession } from "./channel-session.js";
|
|
7
9
|
import { NOT_INVITED, SIGN_IN_LINK_INVALID, SegmentStreamApiError, apiErrorFrom, readJson, } from "./errors.js";
|
|
8
10
|
import { parseMe } from "./me.js";
|
|
9
11
|
import { createOntologyTransport } from "./ontology-transport.js";
|
|
@@ -23,6 +25,7 @@ const ONTOLOGY_PATHS = ["/api/ontology", "/mcp"];
|
|
|
23
25
|
export const OPEN_PATHS = [
|
|
24
26
|
AUTH_API_PATH,
|
|
25
27
|
SIGN_IN_PAGE,
|
|
28
|
+
"/api/segmentstream/configuration",
|
|
26
29
|
"/api/health",
|
|
27
30
|
"/_next",
|
|
28
31
|
"/favicon.ico",
|
|
@@ -79,6 +82,12 @@ class ExpiringCache {
|
|
|
79
82
|
*/
|
|
80
83
|
export function createNeonSession(dependencies) {
|
|
81
84
|
const { config, settings, now } = dependencies;
|
|
85
|
+
const channel = createChannelSession({
|
|
86
|
+
enabled: hasChannelApps(config.channels),
|
|
87
|
+
workspaceId: config.workspaceId,
|
|
88
|
+
ontologyUrl: settings.ontologyUrl,
|
|
89
|
+
fetch: dependencies.fetch,
|
|
90
|
+
});
|
|
82
91
|
const tokens = new ExpiringCache(now);
|
|
83
92
|
const users = new ExpiringCache(now);
|
|
84
93
|
const loadingUsers = new Map();
|
|
@@ -107,17 +116,25 @@ export function createNeonSession(dependencies) {
|
|
|
107
116
|
// before any configuration is needed. Thrown errors pass through untouched: Next.js signals
|
|
108
117
|
// dynamic rendering with them.
|
|
109
118
|
await cookies();
|
|
119
|
+
const channelSession = await channelIdentity();
|
|
120
|
+
if (channelSession) {
|
|
121
|
+
return channelSession;
|
|
122
|
+
}
|
|
110
123
|
const result = await neonAuth().getSession();
|
|
111
124
|
if (result.error !== null) {
|
|
112
125
|
if (result.error.status === 401) {
|
|
113
|
-
return
|
|
126
|
+
return channelIdentity();
|
|
127
|
+
}
|
|
128
|
+
const fallback = await channelIdentity();
|
|
129
|
+
if (fallback) {
|
|
130
|
+
return fallback;
|
|
114
131
|
}
|
|
115
132
|
throw authUnavailable(result.error);
|
|
116
133
|
}
|
|
117
134
|
const session = result.data?.session;
|
|
118
135
|
const user = result.data?.user;
|
|
119
136
|
if (session === null || session === undefined || user === null || user === undefined) {
|
|
120
|
-
return
|
|
137
|
+
return channelIdentity();
|
|
121
138
|
}
|
|
122
139
|
return {
|
|
123
140
|
sessionId: session.id,
|
|
@@ -126,8 +143,23 @@ export function createNeonSession(dependencies) {
|
|
|
126
143
|
name: user.name ?? undefined,
|
|
127
144
|
};
|
|
128
145
|
}
|
|
146
|
+
async function channelIdentity() {
|
|
147
|
+
const session = await channel.read();
|
|
148
|
+
return session
|
|
149
|
+
? {
|
|
150
|
+
sessionId: session.user.id,
|
|
151
|
+
userId: session.user.id,
|
|
152
|
+
name: session.user.name ?? undefined,
|
|
153
|
+
email: session.user.email ?? undefined,
|
|
154
|
+
channel: session,
|
|
155
|
+
}
|
|
156
|
+
: null;
|
|
157
|
+
}
|
|
129
158
|
/** The session's JWT for the ontology service, reused until shortly before it expires. */
|
|
130
159
|
async function sessionToken(session) {
|
|
160
|
+
if (session.channel) {
|
|
161
|
+
return session.channel.token;
|
|
162
|
+
}
|
|
131
163
|
const cached = tokens.get(session.sessionId);
|
|
132
164
|
if (cached !== undefined) {
|
|
133
165
|
return cached;
|
|
@@ -135,7 +167,7 @@ export function createNeonSession(dependencies) {
|
|
|
135
167
|
const result = await neonAuth().token();
|
|
136
168
|
if (result.error !== null) {
|
|
137
169
|
if (result.error.status === 401) {
|
|
138
|
-
return
|
|
170
|
+
return channel.token();
|
|
139
171
|
}
|
|
140
172
|
throw authUnavailable(result.error);
|
|
141
173
|
}
|
|
@@ -171,6 +203,9 @@ export function createNeonSession(dependencies) {
|
|
|
171
203
|
},
|
|
172
204
|
};
|
|
173
205
|
async function currentUser(session) {
|
|
206
|
+
if (session.channel) {
|
|
207
|
+
return session.channel.user;
|
|
208
|
+
}
|
|
174
209
|
const cached = users.get(session.sessionId);
|
|
175
210
|
if (cached !== undefined) {
|
|
176
211
|
return cached;
|
|
@@ -262,6 +297,7 @@ export function createNeonSession(dependencies) {
|
|
|
262
297
|
return user !== null && isAllowed(permission, user);
|
|
263
298
|
}
|
|
264
299
|
async function signOut(options = {}) {
|
|
300
|
+
await channel.signOut();
|
|
265
301
|
const session = await identity().catch(() => null);
|
|
266
302
|
if (session !== null) {
|
|
267
303
|
tokens.delete(session.sessionId);
|
|
@@ -317,6 +353,10 @@ export function createNeonSession(dependencies) {
|
|
|
317
353
|
if (publicPaths.some((path) => matchesPathPrefix(pathname, path))) {
|
|
318
354
|
return NextResponse.next();
|
|
319
355
|
}
|
|
356
|
+
const channelResponse = await channel.proxy(request);
|
|
357
|
+
if (channelResponse) {
|
|
358
|
+
return channelResponse;
|
|
359
|
+
}
|
|
320
360
|
middleware ??= neonAuth().middleware({ loginUrl: SIGN_IN_PAGE });
|
|
321
361
|
const response = await middleware(request);
|
|
322
362
|
if (!isSignInRedirect(request, response)) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { headers } from "next/headers";
|
|
2
|
-
import { trimTrailingSlashes } from "./config.js";
|
|
3
2
|
import { isLoopbackHostname } from "./cookies.js";
|
|
4
3
|
import { SegmentStreamApiError } from "./errors.js";
|
|
5
4
|
/** Every ontology route lives under this path, beside the app on the same origin. */
|
|
@@ -15,6 +14,15 @@ export function createOntologyTransport(options) {
|
|
|
15
14
|
const requestHeaders = new Headers(init.headers);
|
|
16
15
|
const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
17
16
|
requestHeaders.set("authorization", `Bearer ${token}`);
|
|
17
|
+
if (token.startsWith("sso_")) {
|
|
18
|
+
// Keep the browser's public app-session resource when a local proxy uses another origin.
|
|
19
|
+
const incoming = await headers();
|
|
20
|
+
const host = incoming.get("x-forwarded-host") ?? incoming.get("host");
|
|
21
|
+
if (host) {
|
|
22
|
+
requestHeaders.set("x-forwarded-host", host);
|
|
23
|
+
requestHeaders.set("x-forwarded-proto", incoming.get("x-forwarded-proto") ?? "https");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
18
26
|
try {
|
|
19
27
|
return await options.fetch(url, {
|
|
20
28
|
...init,
|
|
@@ -65,12 +73,14 @@ export function createOntologyTransport(options) {
|
|
|
65
73
|
}
|
|
66
74
|
export async function ontologyOrigin(configured) {
|
|
67
75
|
if (configured !== undefined) {
|
|
68
|
-
return
|
|
76
|
+
return new URL(configured).origin;
|
|
69
77
|
}
|
|
70
78
|
// Vercel sets Host to the domain the request arrived on, which routes /api/ontology to the service.
|
|
71
79
|
const headerStore = await headers();
|
|
72
|
-
const host = headerStore.get("host")
|
|
73
|
-
|
|
80
|
+
const host = (headerStore.get("x-forwarded-host") ?? headerStore.get("host"))
|
|
81
|
+
?.split(",")[0]
|
|
82
|
+
?.trim();
|
|
83
|
+
if (host === undefined || host === "") {
|
|
74
84
|
throw new SegmentStreamApiError({
|
|
75
85
|
code: "ontology_url_unknown",
|
|
76
86
|
status: 500,
|
|
@@ -83,7 +93,7 @@ export async function ontologyOrigin(configured) {
|
|
|
83
93
|
: isLoopbackHostname(hostnameOf(host))
|
|
84
94
|
? "http"
|
|
85
95
|
: "https";
|
|
86
|
-
return `${protocol}://${host}
|
|
96
|
+
return new URL(`${protocol}://${host}`).origin;
|
|
87
97
|
}
|
|
88
98
|
function hostnameOf(host) {
|
|
89
99
|
try {
|
package/dist/proxy.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
-
import { trimTrailingSlashes } from "./config.js";
|
|
2
|
+
import { hasChannelApps, trimTrailingSlashes } from "./config.js";
|
|
3
3
|
import { clearSessionCookies, cookiePolicyFor, replaceRequestCookies, safeReturnTo, setSessionCookies, } from "./cookies.js";
|
|
4
4
|
import { SDK_ROUTE_PREFIX, isApiRequest, signInRedirect, unauthenticatedResponse, } from "./sign-in.js";
|
|
5
5
|
export function createProxy(context, options = {}) {
|
|
@@ -37,6 +37,9 @@ export function createProxy(context, options = {}) {
|
|
|
37
37
|
if (verification.status === "valid") {
|
|
38
38
|
return NextResponse.next();
|
|
39
39
|
}
|
|
40
|
+
if (verification.status === "unavailable" && hasChannelApps(context.config.channels)) {
|
|
41
|
+
return sessionUnavailable(context, request, policy);
|
|
42
|
+
}
|
|
40
43
|
}
|
|
41
44
|
const refreshToken = request.cookies.get(policy.names.refresh)?.value;
|
|
42
45
|
if (refreshToken === undefined || refreshToken === "") {
|
|
@@ -83,11 +86,12 @@ export function wellKnownResponse(request, ontologyUrl) {
|
|
|
83
86
|
return NextResponse.json({ error: "not_found" }, { status: 404 });
|
|
84
87
|
}
|
|
85
88
|
/**
|
|
86
|
-
* SegmentStream is unreachable.
|
|
87
|
-
* which ends on the callback's error page
|
|
89
|
+
* SegmentStream is unreachable. APIs and opted-in channel apps get a retryable 503. Other legacy
|
|
90
|
+
* pages restart sign-in, which ends on the callback's error page. Channel apps must not exchange a
|
|
91
|
+
* different identity after failed verification. Session cookies are kept.
|
|
88
92
|
*/
|
|
89
93
|
async function sessionUnavailable(context, request, policy) {
|
|
90
|
-
if (isApiRequest(request)) {
|
|
94
|
+
if (isApiRequest(request) || hasChannelApps(context.config.channels)) {
|
|
91
95
|
return NextResponse.json({ error: "session_refresh_unavailable" }, { status: 503, headers: { "cache-control": "no-store", "retry-after": "5" } });
|
|
92
96
|
}
|
|
93
97
|
return startSignIn(context, request, policy, false);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type SegmentStreamProxy } from "./proxy.js";
|
|
2
|
+
/**
|
|
3
|
+
* An unauthenticated Telegram Web iframe cannot visit the unframeable sign-in page. Keep the
|
|
4
|
+
* protected URL and return only an exchange script; never let the workspace render anonymously.
|
|
5
|
+
*/
|
|
6
|
+
export declare function withTelegramLaunch(proxy: SegmentStreamProxy, channels: Readonly<Record<string, boolean>> | undefined): SegmentStreamProxy;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { CHANNEL_SESSION_PATH, channelSignInPath } from "./channel-protocol.js";
|
|
3
|
+
import { isDecisionPage } from "./decision-pages.js";
|
|
4
|
+
import { randomToken } from "./pkce.js";
|
|
5
|
+
import { matchesPathPrefix } from "./proxy.js";
|
|
6
|
+
import { channelAppFraming } from "./telegram.js";
|
|
7
|
+
/**
|
|
8
|
+
* An unauthenticated Telegram Web iframe cannot visit the unframeable sign-in page. Keep the
|
|
9
|
+
* protected URL and return only an exchange script; never let the workspace render anonymously.
|
|
10
|
+
*/
|
|
11
|
+
export function withTelegramLaunch(proxy, channels) {
|
|
12
|
+
if (channels?.telegram !== true) {
|
|
13
|
+
return proxy;
|
|
14
|
+
}
|
|
15
|
+
return async (request) => {
|
|
16
|
+
const response = await proxy(request);
|
|
17
|
+
const pathname = request.nextUrl.pathname;
|
|
18
|
+
const location = response.headers.get("location");
|
|
19
|
+
const destination = location === null ? null : URL.parse(location, request.url);
|
|
20
|
+
if (request.method !== "GET" ||
|
|
21
|
+
request.headers.get("sec-fetch-dest") !== "iframe" ||
|
|
22
|
+
request.headers.get("sec-fetch-mode") !== "navigate" ||
|
|
23
|
+
isDecisionPage(pathname) ||
|
|
24
|
+
["/api", "/mcp", "/.well-known", "/_next"].some((path) => matchesPathPrefix(pathname, path)) ||
|
|
25
|
+
response.status < 300 ||
|
|
26
|
+
response.status >= 400 ||
|
|
27
|
+
destination?.origin !== request.nextUrl.origin ||
|
|
28
|
+
destination.pathname !== "/sign-in") {
|
|
29
|
+
return response;
|
|
30
|
+
}
|
|
31
|
+
const nonce = randomToken();
|
|
32
|
+
// Only SDK-owned constants enter the script. Launch data stays in the browser until the POST.
|
|
33
|
+
const script = `
|
|
34
|
+
void (async () => {
|
|
35
|
+
const state = document.documentElement.dataset;
|
|
36
|
+
const fail = (code) => {
|
|
37
|
+
state.channelSignIn = "error";
|
|
38
|
+
state.channelSignInError = code;
|
|
39
|
+
};
|
|
40
|
+
const assertion = new URLSearchParams(location.hash.slice(1)).get("tgWebAppData");
|
|
41
|
+
if (!assertion) {
|
|
42
|
+
state.channelSignIn = "idle";
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
state.channelSignIn = "pending";
|
|
46
|
+
try {
|
|
47
|
+
// Persist only a digest, scoped to this tab and exact launch, before the one allowed reload.
|
|
48
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(location.href));
|
|
49
|
+
const reloadKey = "segmentstream:telegram-launch:" + Array.from(new Uint8Array(digest),
|
|
50
|
+
(byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
51
|
+
if (sessionStorage.getItem(reloadKey)) {
|
|
52
|
+
fail("launch_loop");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const exchanged = await fetch(${JSON.stringify(channelSignInPath("telegram"))}, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
credentials: "same-origin",
|
|
58
|
+
redirect: "error",
|
|
59
|
+
headers: { "content-type": "application/json" },
|
|
60
|
+
body: JSON.stringify({ assertion }),
|
|
61
|
+
});
|
|
62
|
+
if (!exchanged.ok) {
|
|
63
|
+
const body = await exchanged.json();
|
|
64
|
+
fail(typeof body.error === "string" ? body.error : "sign_in_failed");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const discovered = await fetch(${JSON.stringify(CHANNEL_SESSION_PATH)}, {
|
|
68
|
+
credentials: "same-origin",
|
|
69
|
+
cache: "no-store",
|
|
70
|
+
redirect: "error",
|
|
71
|
+
});
|
|
72
|
+
const body = discovered.ok ? await discovered.json() : null;
|
|
73
|
+
if (typeof body?.session?.id !== "string" || typeof body?.user?.id !== "string") {
|
|
74
|
+
fail("session_unavailable");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
sessionStorage.setItem(reloadKey, "1");
|
|
78
|
+
location.reload();
|
|
79
|
+
} catch {
|
|
80
|
+
fail("sign_in_unavailable");
|
|
81
|
+
}
|
|
82
|
+
})();`;
|
|
83
|
+
const shell = new NextResponse(`<!doctype html><html><head><meta charset="utf-8"></head><body><script nonce="${nonce}">${script}</script></body></html>`, {
|
|
84
|
+
headers: {
|
|
85
|
+
"content-type": "text/html; charset=utf-8",
|
|
86
|
+
"cache-control": "no-store",
|
|
87
|
+
"referrer-policy": "no-referrer",
|
|
88
|
+
"x-content-type-options": "nosniff",
|
|
89
|
+
"content-security-policy": `default-src 'none'; script-src 'nonce-${nonce}'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors ${channelAppFraming(channels)}`,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
// Preserve stale-session clearing without carrying redirect or middleware bypass headers.
|
|
93
|
+
for (const cookie of response.headers.getSetCookie()) {
|
|
94
|
+
shell.headers.append("set-cookie", cookie);
|
|
95
|
+
}
|
|
96
|
+
return shell;
|
|
97
|
+
};
|
|
98
|
+
}
|
package/dist/telegram.js
ADDED
package/dist/ui/components.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface CanProps extends ChildrenProps {
|
|
|
17
17
|
* ontology service checks every read and action itself.
|
|
18
18
|
*/
|
|
19
19
|
export declare function Can({ permission, children, fallback }: CanProps): ReactNode;
|
|
20
|
-
/** Ends the
|
|
20
|
+
/** Ends the selected session in this browser, then opens `redirectTo`. */
|
|
21
21
|
export declare function signOut(redirectTo?: string): Promise<void>;
|
|
22
22
|
export interface UserMenuProps {
|
|
23
23
|
/** Where to go after signing out. Defaults to `/sign-in`. */
|
package/dist/ui/components.js
CHANGED
|
@@ -4,7 +4,7 @@ import { useState } from "react";
|
|
|
4
4
|
import { safeReturnTo } from "../return-to.js";
|
|
5
5
|
import { usePermissions, useSession, useUser } from "./hooks.js";
|
|
6
6
|
import { navigate } from "./navigation.js";
|
|
7
|
-
import { SIGN_IN_PAGE,
|
|
7
|
+
import { SIGN_IN_PAGE, endBrowserSession, clearSession, getSnapshot } from "./store.js";
|
|
8
8
|
/** Renders its children when a Neon Auth session exists in this browser. */
|
|
9
9
|
export function SignedIn({ children }) {
|
|
10
10
|
return useSession().status === "signed-in" ? children : null;
|
|
@@ -24,10 +24,17 @@ export function Can({ permission, children, fallback = null }) {
|
|
|
24
24
|
}
|
|
25
25
|
return can(permission) ? children : fallback;
|
|
26
26
|
}
|
|
27
|
-
/** Ends the
|
|
27
|
+
/** Ends the selected session in this browser, then opens `redirectTo`. */
|
|
28
28
|
export async function signOut(redirectTo = SIGN_IN_PAGE) {
|
|
29
|
+
const session = getSnapshot().session;
|
|
30
|
+
if (session.status === "signed-in" && session.source === "channel") {
|
|
31
|
+
await endBrowserSession();
|
|
32
|
+
clearSession();
|
|
33
|
+
navigate(safeReturnTo(redirectTo));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
29
36
|
try {
|
|
30
|
-
await
|
|
37
|
+
await endBrowserSession();
|
|
31
38
|
}
|
|
32
39
|
finally {
|
|
33
40
|
clearSession();
|
package/dist/ui/hooks.js
CHANGED
|
@@ -27,9 +27,9 @@ export function useUser() {
|
|
|
27
27
|
const sessionId = session.status === "signed-in" ? session.sessionId : null;
|
|
28
28
|
useEffect(() => {
|
|
29
29
|
if (sessionId !== null) {
|
|
30
|
-
ensureUser(sessionId);
|
|
30
|
+
ensureUser(sessionId, session.status === "signed-in" ? session.source : undefined);
|
|
31
31
|
}
|
|
32
|
-
}, [sessionId]);
|
|
32
|
+
}, [sessionId, session]);
|
|
33
33
|
switch (session.status) {
|
|
34
34
|
case "loading":
|
|
35
35
|
return LOADING_USER;
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type { AgentConnectProps } from "./agent-connect.js";
|
|
|
5
5
|
export { usePermissions, useSession, useUser } from "./hooks.js";
|
|
6
6
|
export type { Permissions } from "./hooks.js";
|
|
7
7
|
export { SignIn } from "./sign-in.js";
|
|
8
|
+
export { TelegramSignIn, useTelegramSignIn } from "./telegram-sign-in.js";
|
|
9
|
+
export type { TelegramSignInProps, TelegramSignInOptions } from "./telegram-sign-in.js";
|
|
8
10
|
export type { SignInProps } from "./sign-in.js";
|
|
9
11
|
export { SIGN_IN_ERROR_MESSAGES, signInErrorCode, signInErrorMessage, useSignIn, } from "./use-sign-in.js";
|
|
10
12
|
export type { SignInController, SignInErrorCode, SignInNoticeCode, SignInOptions, } from "./use-sign-in.js";
|
|
@@ -14,3 +16,5 @@ export type { CurrentUser } from "../me.js";
|
|
|
14
16
|
export type { AgentPairing } from "../agent.js";
|
|
15
17
|
export { SegmentStreamApiError, NOT_INVITED, SIGN_IN_LINK_INVALID } from "../errors.js";
|
|
16
18
|
export { SegmentStreamLogo } from "./segmentstream-logo.js";
|
|
19
|
+
export { useChannelSignIn } from "./use-channel-sign-in.js";
|
|
20
|
+
export type { ChannelSignInState, ChannelSignInOptions } from "./use-channel-sign-in.js";
|
package/dist/ui/index.js
CHANGED
|
@@ -6,7 +6,9 @@ export { Can, SignedIn, SignedOut, UserMenu, signOut } from "./components.js";
|
|
|
6
6
|
export { AgentConnect } from "./agent-connect.js";
|
|
7
7
|
export { usePermissions, useSession, useUser } from "./hooks.js";
|
|
8
8
|
export { SignIn } from "./sign-in.js";
|
|
9
|
+
export { TelegramSignIn, useTelegramSignIn } from "./telegram-sign-in.js";
|
|
9
10
|
export { SIGN_IN_ERROR_MESSAGES, signInErrorCode, signInErrorMessage, useSignIn, } from "./use-sign-in.js";
|
|
10
11
|
export { isAllowed } from "../permissions.js";
|
|
11
12
|
export { SegmentStreamApiError, NOT_INVITED, SIGN_IN_LINK_INVALID } from "../errors.js";
|
|
12
13
|
export { SegmentStreamLogo } from "./segmentstream-logo.js";
|
|
14
|
+
export { useChannelSignIn } from "./use-channel-sign-in.js";
|