@segmentstream/app 0.0.0-stage → 0.38.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/LICENSE +201 -0
- package/NOTICE +2 -0
- package/README.md +1236 -2
- package/dist/agent.d.ts +56 -0
- package/dist/agent.js +203 -0
- package/dist/app.d.ts +82 -0
- package/dist/app.js +434 -0
- package/dist/browser-auth-capability.d.ts +2 -0
- package/dist/browser-auth-capability.js +2 -0
- package/dist/browser-revision.d.ts +19 -0
- package/dist/browser-revision.js +140 -0
- 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 +53 -0
- package/dist/config.js +112 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.js +1 -0
- package/dist/cookies.d.ts +56 -0
- package/dist/cookies.js +131 -0
- package/dist/data.d.ts +233 -0
- package/dist/data.js +206 -0
- package/dist/decision-pages.d.ts +14 -0
- package/dist/decision-pages.js +87 -0
- package/dist/deployment.d.ts +30 -0
- package/dist/deployment.js +72 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.js +123 -0
- package/dist/federation.d.ts +24 -0
- package/dist/federation.js +613 -0
- package/dist/groups.d.ts +95 -0
- package/dist/groups.js +280 -0
- package/dist/handlers.d.ts +10 -0
- package/dist/handlers.js +161 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +29 -0
- package/dist/jobs.d.ts +49 -0
- package/dist/jobs.js +40 -0
- package/dist/legacy.d.ts +77 -0
- package/dist/legacy.js +57 -0
- package/dist/logo.d.ts +1 -0
- package/dist/logo.js +25 -0
- package/dist/me.d.ts +42 -0
- package/dist/me.js +54 -0
- package/dist/navigation/build.d.ts +68 -0
- package/dist/navigation/build.js +290 -0
- package/dist/navigation/cli.d.ts +19 -0
- package/dist/navigation/cli.js +37 -0
- package/dist/navigation/generate.d.ts +43 -0
- package/dist/navigation/generate.js +138 -0
- package/dist/navigation/next.d.ts +10 -0
- package/dist/navigation/next.js +18 -0
- package/dist/navigation-protocol.d.ts +55 -0
- package/dist/navigation-protocol.js +298 -0
- package/dist/neon.d.ts +101 -0
- package/dist/neon.js +539 -0
- package/dist/oauth-completion.d.ts +6 -0
- package/dist/oauth-completion.js +106 -0
- package/dist/oauth.d.ts +59 -0
- package/dist/oauth.js +129 -0
- package/dist/ontology-names.d.ts +10 -0
- package/dist/ontology-names.js +38 -0
- package/dist/ontology-transport.d.ts +34 -0
- package/dist/ontology-transport.js +130 -0
- package/dist/ontology-wire.d.ts +66 -0
- package/dist/ontology-wire.js +355 -0
- package/dist/ontology.d.ts +381 -0
- package/dist/ontology.js +367 -0
- package/dist/pages.d.ts +6 -0
- package/dist/pages.js +6 -0
- package/dist/parameters.d.ts +55 -0
- package/dist/parameters.js +118 -0
- package/dist/permissions.d.ts +19 -0
- package/dist/permissions.js +55 -0
- package/dist/pkce.d.ts +8 -0
- package/dist/pkce.js +40 -0
- package/dist/proxy.d.ts +25 -0
- package/dist/proxy.js +135 -0
- package/dist/request-timing.d.ts +6 -0
- package/dist/request-timing.js +53 -0
- package/dist/return-to.d.ts +2 -0
- package/dist/return-to.js +24 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +80 -0
- package/dist/sessions.d.ts +27 -0
- package/dist/sessions.js +72 -0
- package/dist/sign-in.d.ts +37 -0
- package/dist/sign-in.js +95 -0
- package/dist/storage.d.ts +79 -0
- package/dist/storage.js +180 -0
- package/dist/telegram-launch.d.ts +7 -0
- package/dist/telegram-launch.js +134 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +6 -0
- package/dist/tokens.d.ts +51 -0
- package/dist/tokens.js +70 -0
- package/dist/ui/agent-connect.d.ts +18 -0
- package/dist/ui/agent-connect.js +23 -0
- package/dist/ui/components.d.ts +27 -0
- package/dist/ui/components.js +71 -0
- package/dist/ui/hooks.d.ts +19 -0
- package/dist/ui/hooks.js +56 -0
- package/dist/ui/index.d.ts +22 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/navigation.d.ts +5 -0
- package/dist/ui/navigation.js +8 -0
- package/dist/ui/segmentstream-logo.d.ts +2 -0
- package/dist/ui/segmentstream-logo.js +7 -0
- package/dist/ui/sign-in-check.d.ts +14 -0
- package/dist/ui/sign-in-check.js +41 -0
- package/dist/ui/sign-in.d.ts +17 -0
- package/dist/ui/sign-in.js +65 -0
- package/dist/ui/store.d.ts +80 -0
- package/dist/ui/store.js +280 -0
- package/dist/ui/styles.css +249 -0
- package/dist/ui/telegram-sign-in.d.ts +16 -0
- package/dist/ui/telegram-sign-in.js +21 -0
- package/dist/ui/use-browser-sign-in.d.ts +52 -0
- package/dist/ui/use-browser-sign-in.js +169 -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.d.ts +109 -0
- package/dist/ui/use-sign-in.js +304 -0
- package/package.json +95 -4
package/dist/app.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { createBrowserConfigurationCheck, resolveBrowserRevision } from "./browser-revision.js";
|
|
2
|
+
import { createNeonAuth } from "@neondatabase/auth/next/server";
|
|
3
|
+
import { deploymentFromEnv, DeploymentAdmissionError, requireDeploymentCapability, } from "./deployment.js";
|
|
4
|
+
import { redirect } from "next/navigation";
|
|
5
|
+
import { NextResponse } from "next/server";
|
|
6
|
+
import { environmentValue, hasChannelApps, processEnvironment, resolveConfig, resolveNeonAuth, resolveBrowserAuth, } from "./config.js";
|
|
7
|
+
import { createAgentIdentities } from "./agent.js";
|
|
8
|
+
import { channelAppFraming } from "./telegram.js";
|
|
9
|
+
import { withTelegramLaunch } from "./telegram-launch.js";
|
|
10
|
+
import { createChannelSession } from "./channel-session.js";
|
|
11
|
+
import { createDataClient } from "./data.js";
|
|
12
|
+
import { withUnframeablePages } from "./decision-pages.js";
|
|
13
|
+
import { SegmentStreamApiError } from "./errors.js";
|
|
14
|
+
import { createHandlers } from "./handlers.js";
|
|
15
|
+
import { createNeonSession } from "./neon.js";
|
|
16
|
+
import { createAgentAuthorizations } from "./oauth.js";
|
|
17
|
+
import { createOntologyClient } from "./ontology.js";
|
|
18
|
+
import { createOntologyTransport } from "./ontology-transport.js";
|
|
19
|
+
import { createProxy, matchesPathPrefix, normalizePublicPath, wellKnownResponse } from "./proxy.js";
|
|
20
|
+
import { revisionUnavailableResponse, SDK_ROUTE_PREFIX } from "./sign-in.js";
|
|
21
|
+
import { createServerSession } from "./server.js";
|
|
22
|
+
import { createSessionClient } from "./sessions.js";
|
|
23
|
+
import { withFederation } from "./federation.js";
|
|
24
|
+
import { createTokenVerifier, remoteKeySet } from "./tokens.js";
|
|
25
|
+
const NEON_AUTH_NOT_CONFIGURED = "email_sign_in_disabled";
|
|
26
|
+
export function createApp(config, dependencies = {}) {
|
|
27
|
+
const environment = dependencies.environment ?? processEnvironment();
|
|
28
|
+
const deployment = deploymentFromEnv(environment);
|
|
29
|
+
const resolved = resolveConfig(deployment.target === "preview" ? { ...config, channels: undefined } : config, environment);
|
|
30
|
+
const browserAuth = resolveBrowserAuth(environment);
|
|
31
|
+
const neonAuth = browserAuth && !browserAuth.browser.methods.includes("email")
|
|
32
|
+
? null
|
|
33
|
+
: resolveNeonAuth(environment);
|
|
34
|
+
if (!browserAuth &&
|
|
35
|
+
deployment.target === "preview" &&
|
|
36
|
+
(!neonAuth || !neonAuth.cookieSecret || neonAuth.cookieSecret.length < 32)) {
|
|
37
|
+
throw new DeploymentAdmissionError("preview_configuration_invalid");
|
|
38
|
+
}
|
|
39
|
+
if (browserAuth?.browser.methods.includes("email") && !neonAuth)
|
|
40
|
+
throw new Error("browser_auth_provider_unavailable");
|
|
41
|
+
const keySet = dependencies.keySet ?? remoteKeySet(resolved.baseUrl);
|
|
42
|
+
const rawFetch = dependencies.fetch ?? ((input, init) => fetch(input, init));
|
|
43
|
+
const fetchImplementation = async (input, init) => {
|
|
44
|
+
requireDeploymentCapability(deployment, undefined, (dependencies.now ?? Date.now)());
|
|
45
|
+
return rawFetch(input, init);
|
|
46
|
+
};
|
|
47
|
+
const sessionClient = createSessionClient(resolved, fetchImplementation);
|
|
48
|
+
const issuer = environmentValue(environment, "SEGMENTSTREAM_OIDC_ISSUER");
|
|
49
|
+
const clientId = environmentValue(environment, "SEGMENTSTREAM_OIDC_CLIENT_ID");
|
|
50
|
+
const browserRevision = browserAuth ? resolveBrowserRevision(environment, issuer) : undefined;
|
|
51
|
+
const withChannelLaunch = (app) => ({
|
|
52
|
+
...app,
|
|
53
|
+
proxy: (options) => withTelegramLaunch(app.proxy(options), resolved.channels, options),
|
|
54
|
+
});
|
|
55
|
+
const emailOnly = (app) => {
|
|
56
|
+
if (!browserAuth)
|
|
57
|
+
return app;
|
|
58
|
+
const verify = createBrowserConfigurationCheck(fetchImplementation, browserAuth, browserRevision, environmentValue(environment, "SEGMENTSTREAM_ONTOLOGY_URL"));
|
|
59
|
+
const guardAuthHandler = (handler) => async (request, context) => {
|
|
60
|
+
try {
|
|
61
|
+
await verify();
|
|
62
|
+
}
|
|
63
|
+
catch (cause) {
|
|
64
|
+
return revisionUnavailableResponse(request, cause);
|
|
65
|
+
}
|
|
66
|
+
return handler(request, context);
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
...app,
|
|
70
|
+
authHandlers: () => {
|
|
71
|
+
const handlers = app.authHandlers();
|
|
72
|
+
return {
|
|
73
|
+
GET: guardAuthHandler(handlers.GET),
|
|
74
|
+
POST: guardAuthHandler(handlers.POST),
|
|
75
|
+
PUT: guardAuthHandler(handlers.PUT),
|
|
76
|
+
DELETE: guardAuthHandler(handlers.DELETE),
|
|
77
|
+
PATCH: guardAuthHandler(handlers.PATCH),
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
getUser: async (options) => {
|
|
81
|
+
await verify();
|
|
82
|
+
return app.getUser(options);
|
|
83
|
+
},
|
|
84
|
+
requireUser: async (options) => {
|
|
85
|
+
await verify();
|
|
86
|
+
return app.requireUser(options);
|
|
87
|
+
},
|
|
88
|
+
can: async (permission, options) => {
|
|
89
|
+
await verify();
|
|
90
|
+
return app.can(permission, options);
|
|
91
|
+
},
|
|
92
|
+
proxy: (options) => async (request) => {
|
|
93
|
+
try {
|
|
94
|
+
await verify();
|
|
95
|
+
}
|
|
96
|
+
catch (cause) {
|
|
97
|
+
return revisionUnavailableResponse(request, cause);
|
|
98
|
+
}
|
|
99
|
+
return app.proxy(options)(request);
|
|
100
|
+
},
|
|
101
|
+
handlers: () => ({
|
|
102
|
+
...app.handlers(),
|
|
103
|
+
GET: async (request) => {
|
|
104
|
+
try {
|
|
105
|
+
await verify();
|
|
106
|
+
}
|
|
107
|
+
catch (cause) {
|
|
108
|
+
return revisionUnavailableResponse(request, cause);
|
|
109
|
+
}
|
|
110
|
+
if (request.nextUrl.pathname === `${SDK_ROUTE_PREFIX}/configuration`)
|
|
111
|
+
return NextResponse.json({
|
|
112
|
+
enabled: false,
|
|
113
|
+
neon: true,
|
|
114
|
+
channels: resolved.channels ?? {},
|
|
115
|
+
browser: browserAuth.browser,
|
|
116
|
+
}, { headers: { "cache-control": "no-store" } });
|
|
117
|
+
return app.handlers().GET(request);
|
|
118
|
+
},
|
|
119
|
+
POST: async (request) => {
|
|
120
|
+
try {
|
|
121
|
+
await verify();
|
|
122
|
+
}
|
|
123
|
+
catch (cause) {
|
|
124
|
+
return revisionUnavailableResponse(request, cause);
|
|
125
|
+
}
|
|
126
|
+
return app.handlers().POST(request);
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const federated = (app, proofTransport, identityToken, channelSession) => {
|
|
132
|
+
if (!browserAuth && deployment.target === "preview") {
|
|
133
|
+
return previewApp(app, deployment, dependencies.now ?? Date.now);
|
|
134
|
+
}
|
|
135
|
+
if (browserAuth?.browser.methods.includes("segmentstream") && (!issuer || !clientId)) {
|
|
136
|
+
throw new Error("browser_auth_provider_unavailable");
|
|
137
|
+
}
|
|
138
|
+
if (browserAuth && !browserAuth.browser.methods.includes("segmentstream"))
|
|
139
|
+
return withChannelLaunch(emailOnly(app));
|
|
140
|
+
if (!issuer && !clientId) {
|
|
141
|
+
return withChannelLaunch(app);
|
|
142
|
+
}
|
|
143
|
+
if (!issuer || !clientId) {
|
|
144
|
+
throw new Error("SEGMENTSTREAM_OIDC_ISSUER and SEGMENTSTREAM_OIDC_CLIENT_ID must be configured together.");
|
|
145
|
+
}
|
|
146
|
+
const federatedApp = withChannelLaunch(withFederation(app, resolved, { issuer, clientId }, {
|
|
147
|
+
fetch: fetchImplementation,
|
|
148
|
+
browserAuth,
|
|
149
|
+
browserRevision,
|
|
150
|
+
ontologyUrl: environmentValue(environment, "SEGMENTSTREAM_ONTOLOGY_URL"),
|
|
151
|
+
neon: neonAuth !== null,
|
|
152
|
+
proofTransport,
|
|
153
|
+
identityToken,
|
|
154
|
+
channelSession,
|
|
155
|
+
}));
|
|
156
|
+
return deployment.target === "preview"
|
|
157
|
+
? previewApp(federatedApp, deployment, dependencies.now ?? Date.now, true)
|
|
158
|
+
: federatedApp;
|
|
159
|
+
};
|
|
160
|
+
const context = {
|
|
161
|
+
config: resolved,
|
|
162
|
+
verifyToken: createTokenVerifier(resolved, keySet),
|
|
163
|
+
requestToken: sessionClient.requestToken,
|
|
164
|
+
revokeSession: sessionClient.revoke,
|
|
165
|
+
};
|
|
166
|
+
const legacy = createServerSession(context);
|
|
167
|
+
const getAppCredential = () => environmentValue(environment, "SEGMENTSTREAM_APP_CREDENTIAL");
|
|
168
|
+
if (neonAuth === null) {
|
|
169
|
+
const data = createDataClient({
|
|
170
|
+
config: resolved,
|
|
171
|
+
fetch: fetchImplementation,
|
|
172
|
+
getAccessToken: legacy.getAccessToken,
|
|
173
|
+
getAppCredential,
|
|
174
|
+
});
|
|
175
|
+
const ontologyUrl = environmentValue(environment, "SEGMENTSTREAM_ONTOLOGY_URL");
|
|
176
|
+
const channel = createChannelSession({
|
|
177
|
+
enabled: hasChannelApps(resolved.channels),
|
|
178
|
+
workspaceId: resolved.workspaceId,
|
|
179
|
+
ontologyUrl,
|
|
180
|
+
fetch: fetchImplementation,
|
|
181
|
+
});
|
|
182
|
+
const getChannelUser = async () => {
|
|
183
|
+
const session = await channel.read();
|
|
184
|
+
return session
|
|
185
|
+
? {
|
|
186
|
+
...session.user,
|
|
187
|
+
email: session.user.email ?? undefined,
|
|
188
|
+
name: session.user.name ?? undefined,
|
|
189
|
+
organizationId: resolved.organizationId,
|
|
190
|
+
workspaceId: resolved.workspaceId,
|
|
191
|
+
}
|
|
192
|
+
: null;
|
|
193
|
+
};
|
|
194
|
+
const getUser = async () => (await getChannelUser()) ?? legacy.getUser();
|
|
195
|
+
// @legacy app.legacy-segmentstream-sign-in
|
|
196
|
+
// The ontology service accepts SegmentStream app tokens while workspaces move to Neon Auth.
|
|
197
|
+
const ontologyTransport = createOntologyTransport({
|
|
198
|
+
fetch: fetchImplementation,
|
|
199
|
+
ontologyUrl: ontologyUrl === undefined ? undefined : new URL(ontologyUrl).origin,
|
|
200
|
+
credentials: { token: async () => (await channel.read())?.token ?? legacy.getAccessToken() },
|
|
201
|
+
});
|
|
202
|
+
return federated({
|
|
203
|
+
authentication: "segmentstream",
|
|
204
|
+
deployment,
|
|
205
|
+
proxy: (options) => {
|
|
206
|
+
const primaryProxy = createProxy(context, options);
|
|
207
|
+
const publicPaths = [SDK_ROUTE_PREFIX, ...(options?.publicPaths ?? [])].map(normalizePublicPath);
|
|
208
|
+
return withUnframeablePages(async (request) => {
|
|
209
|
+
const discovery = wellKnownResponse(request, ontologyUrl);
|
|
210
|
+
if (discovery !== undefined) {
|
|
211
|
+
return discovery;
|
|
212
|
+
}
|
|
213
|
+
if (request.nextUrl.pathname.startsWith("/api/ontology/")) {
|
|
214
|
+
return ontologyUrl
|
|
215
|
+
? NextResponse.rewrite(new URL(`${request.nextUrl.pathname}${request.nextUrl.search}`, ontologyUrl))
|
|
216
|
+
: NextResponse.next();
|
|
217
|
+
}
|
|
218
|
+
if (hasChannelApps(resolved.channels) && request.nextUrl.pathname === "/sign-in") {
|
|
219
|
+
return NextResponse.next();
|
|
220
|
+
}
|
|
221
|
+
if (publicPaths.some((path) => matchesPathPrefix(request.nextUrl.pathname, path))) {
|
|
222
|
+
return primaryProxy(request);
|
|
223
|
+
}
|
|
224
|
+
const response = (await channel.proxy(request)) ?? (await primaryProxy(request));
|
|
225
|
+
if (hasChannelApps(resolved.channels) &&
|
|
226
|
+
response.headers.has("location") &&
|
|
227
|
+
!request.nextUrl.pathname.startsWith("/api/")) {
|
|
228
|
+
const destination = new URL("/sign-in", request.url);
|
|
229
|
+
destination.searchParams.set("returnTo", request.nextUrl.pathname + request.nextUrl.search);
|
|
230
|
+
response.headers.set("location", destination.href);
|
|
231
|
+
}
|
|
232
|
+
return response;
|
|
233
|
+
}, channelAppFraming(resolved.channels));
|
|
234
|
+
},
|
|
235
|
+
authHandlers: notConfiguredHandlers,
|
|
236
|
+
handlers: () => createHandlers(context),
|
|
237
|
+
getUser,
|
|
238
|
+
requireUser: async () => {
|
|
239
|
+
if (!hasChannelApps(resolved.channels) && !(await channel.token())) {
|
|
240
|
+
return legacy.requireUser();
|
|
241
|
+
}
|
|
242
|
+
const user = await getUser();
|
|
243
|
+
if (!user) {
|
|
244
|
+
redirect("/sign-in");
|
|
245
|
+
}
|
|
246
|
+
return user;
|
|
247
|
+
},
|
|
248
|
+
can: async () => false,
|
|
249
|
+
signOut: async (options) => {
|
|
250
|
+
await channel.signOut();
|
|
251
|
+
return legacy.signOut(options);
|
|
252
|
+
},
|
|
253
|
+
getAccessToken: legacy.getAccessToken,
|
|
254
|
+
fetch: data.fetch,
|
|
255
|
+
warehouse: data.warehouse,
|
|
256
|
+
jobs: data.jobs,
|
|
257
|
+
ontology: createOntologyClient(ontologyTransport),
|
|
258
|
+
oauth: createAgentAuthorizations(ontologyTransport),
|
|
259
|
+
agent: createAgentIdentities(ontologyTransport),
|
|
260
|
+
}, undefined, undefined, { getUser: getChannelUser, signOut: channel.signOut, transport: channel.transport });
|
|
261
|
+
}
|
|
262
|
+
const neon = createNeonSession({
|
|
263
|
+
config: resolved,
|
|
264
|
+
settings: neonAuth,
|
|
265
|
+
createNeonAuth: dependencies.createNeonAuth ?? createNeonAuth,
|
|
266
|
+
fetch: fetchImplementation,
|
|
267
|
+
now: dependencies.now ?? Date.now,
|
|
268
|
+
});
|
|
269
|
+
const data = createDataClient({
|
|
270
|
+
config: resolved,
|
|
271
|
+
fetch: fetchImplementation,
|
|
272
|
+
getAccessToken: noSegmentStreamSession,
|
|
273
|
+
getAppCredential,
|
|
274
|
+
});
|
|
275
|
+
return federated({
|
|
276
|
+
authentication: "neon-auth",
|
|
277
|
+
deployment,
|
|
278
|
+
proxy: (options) => withUnframeablePages(neon.proxy(options), channelAppFraming(resolved.channels)),
|
|
279
|
+
authHandlers: neon.authHandlers,
|
|
280
|
+
handlers: () => createHandlers(context, { neon: true }),
|
|
281
|
+
getUser: neon.getUser,
|
|
282
|
+
requireUser: neon.requireUser,
|
|
283
|
+
can: neon.can,
|
|
284
|
+
signOut: neon.signOut,
|
|
285
|
+
getAccessToken: async () => null,
|
|
286
|
+
fetch: data.fetch,
|
|
287
|
+
warehouse: data.warehouse,
|
|
288
|
+
jobs: data.jobs,
|
|
289
|
+
ontology: createOntologyClient(neon.ontologyTransport),
|
|
290
|
+
oauth: createAgentAuthorizations(neon.ontologyTransport),
|
|
291
|
+
agent: createAgentIdentities(neon.ontologyTransport),
|
|
292
|
+
}, neon.ontologyTransport, neon.identityToken);
|
|
293
|
+
}
|
|
294
|
+
/** Guard every server method, including clients captured before the preview expires. */
|
|
295
|
+
function previewApp(app, deployment, now, federationEnabled = false) {
|
|
296
|
+
const admit = (capability) => requireDeploymentCapability(deployment, capability, now());
|
|
297
|
+
const guarded = (client, capability) => new Proxy(client, {
|
|
298
|
+
get(target, key, receiver) {
|
|
299
|
+
const value = Reflect.get(target, key, receiver);
|
|
300
|
+
if (value && typeof value === "object")
|
|
301
|
+
return guarded(value, capability);
|
|
302
|
+
if (typeof value !== "function")
|
|
303
|
+
return value;
|
|
304
|
+
return (...args) => {
|
|
305
|
+
try {
|
|
306
|
+
admit(capability);
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
return Promise.reject(error);
|
|
310
|
+
}
|
|
311
|
+
const result = Reflect.apply(value, target, args);
|
|
312
|
+
return result && typeof result === "object" && !(result instanceof Promise)
|
|
313
|
+
? guarded(result)
|
|
314
|
+
: result;
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
const refusal = (error) => NextResponse.json({ error: error.code, ...(error.capability ? { capability: error.capability } : {}) }, { status: error.status, headers: { "cache-control": "no-store" } });
|
|
319
|
+
const authHandlers = () => Object.fromEntries(Object.entries(app.authHandlers()).map(([method, handler]) => [
|
|
320
|
+
method,
|
|
321
|
+
async (...args) => {
|
|
322
|
+
try {
|
|
323
|
+
admit();
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
return refusal(error);
|
|
327
|
+
}
|
|
328
|
+
return handler(...args);
|
|
329
|
+
},
|
|
330
|
+
]));
|
|
331
|
+
const deniedLegacy = async () => {
|
|
332
|
+
try {
|
|
333
|
+
admit();
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
return refusal(error);
|
|
337
|
+
}
|
|
338
|
+
return refusal(new DeploymentAdmissionError("preview_authentication_unavailable"));
|
|
339
|
+
};
|
|
340
|
+
const server = guarded(app);
|
|
341
|
+
const deniedAgentHandler = async () => {
|
|
342
|
+
try {
|
|
343
|
+
admit("agents");
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
return refusal(error);
|
|
347
|
+
}
|
|
348
|
+
return refusal(new DeploymentAdmissionError("preview_capability_unavailable", "agents"));
|
|
349
|
+
};
|
|
350
|
+
return {
|
|
351
|
+
...app,
|
|
352
|
+
getUser: server.getUser,
|
|
353
|
+
requireUser: server.requireUser,
|
|
354
|
+
can: server.can,
|
|
355
|
+
signOut: server.signOut,
|
|
356
|
+
getAccessToken: server.getAccessToken,
|
|
357
|
+
fetch: async () => {
|
|
358
|
+
admit();
|
|
359
|
+
throw new DeploymentAdmissionError("preview_authentication_unavailable");
|
|
360
|
+
},
|
|
361
|
+
warehouse: guarded(app.warehouse),
|
|
362
|
+
jobs: guarded(app.jobs, "pipelineRuns"),
|
|
363
|
+
ontology: guarded(app.ontology),
|
|
364
|
+
oauth: {
|
|
365
|
+
...guarded(app.oauth, "agents"),
|
|
366
|
+
consentHandler: () => ({ POST: deniedAgentHandler }),
|
|
367
|
+
},
|
|
368
|
+
agent: {
|
|
369
|
+
...guarded(app.agent, "agents"),
|
|
370
|
+
connectHandler: () => ({ POST: deniedAgentHandler }),
|
|
371
|
+
},
|
|
372
|
+
authHandlers,
|
|
373
|
+
handlers: () => federationEnabled
|
|
374
|
+
? {
|
|
375
|
+
GET: async (request) => {
|
|
376
|
+
try {
|
|
377
|
+
admit();
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
return refusal(error);
|
|
381
|
+
}
|
|
382
|
+
return app.handlers().GET(request);
|
|
383
|
+
},
|
|
384
|
+
POST: async (request) => {
|
|
385
|
+
try {
|
|
386
|
+
admit();
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
return refusal(error);
|
|
390
|
+
}
|
|
391
|
+
return app.handlers().POST(request);
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
: {
|
|
395
|
+
GET: async (request) => {
|
|
396
|
+
if (request.nextUrl.pathname === `${SDK_ROUTE_PREFIX}/configuration`) {
|
|
397
|
+
try {
|
|
398
|
+
admit();
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
return refusal(error);
|
|
402
|
+
}
|
|
403
|
+
return NextResponse.json({ enabled: false, neon: true, channels: {}, deployment }, { headers: { "cache-control": "no-store" } });
|
|
404
|
+
}
|
|
405
|
+
return deniedLegacy();
|
|
406
|
+
},
|
|
407
|
+
POST: deniedLegacy,
|
|
408
|
+
},
|
|
409
|
+
proxy: (options) => {
|
|
410
|
+
const proxy = app.proxy(options);
|
|
411
|
+
return async (request) => {
|
|
412
|
+
try {
|
|
413
|
+
admit();
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
return refusal(error);
|
|
417
|
+
}
|
|
418
|
+
return proxy(request);
|
|
419
|
+
};
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
/** Under Neon Auth the app has no SegmentStream session to call the API with. */
|
|
424
|
+
async function noSegmentStreamSession() {
|
|
425
|
+
throw new SegmentStreamApiError({
|
|
426
|
+
code: "app_credential_missing",
|
|
427
|
+
status: 401,
|
|
428
|
+
detail: "Apps that sign in with Neon Auth have no SegmentStream session. Warehouse queries and opted-in job runs need SEGMENTSTREAM_APP_CREDENTIAL, which SegmentStream sets on deploy. Generic fetch requires a legacy SegmentStream session.",
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
function notConfiguredHandlers() {
|
|
432
|
+
const handler = async () => NextResponse.json({ error: "neon_auth_not_configured", detail: NEON_AUTH_NOT_CONFIGURED }, { status: 404, headers: { "cache-control": "no-store" } });
|
|
433
|
+
return { GET: handler, POST: handler, PUT: handler, DELETE: handler, PATCH: handler };
|
|
434
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BrowserAuthBinding, Environment } from "./config.js";
|
|
2
|
+
import { SegmentStreamApiError } from "./errors.js";
|
|
3
|
+
import type { Fetch } from "./sessions.js";
|
|
4
|
+
export interface BrowserRevision {
|
|
5
|
+
readonly issuer: string;
|
|
6
|
+
readonly clientId: string;
|
|
7
|
+
readonly deploymentId?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function resolveBrowserRevision(environment: Environment, fallbackIssuer?: string): BrowserRevision;
|
|
10
|
+
export interface BrowserRevisionAdmission {
|
|
11
|
+
/** Only Cloud may authorize reconciling this revision with another exact manifest. */
|
|
12
|
+
readonly compatibleManifestDigest?: string;
|
|
13
|
+
readonly validUntil?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function deploymentRefreshRequired(): SegmentStreamApiError;
|
|
16
|
+
/** Share in-flight reads only. A warm process must not extend a retiring revision's deadline. */
|
|
17
|
+
export declare function createBrowserRevisionCheck(fetcher: Fetch, binding: BrowserAuthBinding, revision: BrowserRevision): () => Promise<BrowserRevisionAdmission>;
|
|
18
|
+
/** Reconcile only with the exact live manifest Cloud admitted for this immutable client. */
|
|
19
|
+
export declare function createBrowserConfigurationCheck(fetcher: Fetch, binding: BrowserAuthBinding, revision: BrowserRevision, ontologyUrl?: string): () => Promise<void>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { environmentValue } from "./config.js";
|
|
2
|
+
import { apiErrorFrom, SegmentStreamApiError } from "./errors.js";
|
|
3
|
+
import { ontologyOrigin } from "./ontology-transport.js";
|
|
4
|
+
export function resolveBrowserRevision(environment, fallbackIssuer) {
|
|
5
|
+
const clientId = environmentValue(environment, "SEGMENTSTREAM_BROWSER_AUTH_REVISION_CLIENT_ID");
|
|
6
|
+
const issuer = environmentValue(environment, "SEGMENTSTREAM_BROWSER_AUTH_REVISION_ISSUER") ?? fallbackIssuer;
|
|
7
|
+
if (!clientId || !issuer)
|
|
8
|
+
throw new Error("browser_auth_revision_binding_missing");
|
|
9
|
+
const url = new URL(issuer);
|
|
10
|
+
if (url.protocol !== "https:" ||
|
|
11
|
+
url.username ||
|
|
12
|
+
url.password ||
|
|
13
|
+
url.search ||
|
|
14
|
+
url.hash ||
|
|
15
|
+
issuer.endsWith("/"))
|
|
16
|
+
throw new Error("browser_auth_revision_binding_invalid");
|
|
17
|
+
const deploymentId = environment.SEGMENTSTREAM_BROWSER_AUTH_ENVIRONMENT === "local"
|
|
18
|
+
? undefined
|
|
19
|
+
: environmentValue(environment, "SEGMENTSTREAM_ONTOLOGY_DEPLOYMENT_ID");
|
|
20
|
+
if (!deploymentId && environment.SEGMENTSTREAM_BROWSER_AUTH_ENVIRONMENT !== "local")
|
|
21
|
+
throw new Error("browser_auth_revision_binding_missing");
|
|
22
|
+
return { issuer, clientId, ...(deploymentId ? { deploymentId } : {}) };
|
|
23
|
+
}
|
|
24
|
+
export function deploymentRefreshRequired() {
|
|
25
|
+
return new SegmentStreamApiError({
|
|
26
|
+
code: "deployment_refresh_required",
|
|
27
|
+
status: 409,
|
|
28
|
+
detail: "deployment_refresh_required",
|
|
29
|
+
retryable: false,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Share in-flight reads only. A warm process must not extend a retiring revision's deadline. */
|
|
33
|
+
export function createBrowserRevisionCheck(fetcher, binding, revision) {
|
|
34
|
+
let pending;
|
|
35
|
+
return () => {
|
|
36
|
+
if (!pending) {
|
|
37
|
+
const started = Date.now();
|
|
38
|
+
const check = (async () => {
|
|
39
|
+
try {
|
|
40
|
+
const endpoint = new URL(`${revision.issuer}/revision-status`);
|
|
41
|
+
endpoint.search = new URLSearchParams({
|
|
42
|
+
client_id: revision.clientId,
|
|
43
|
+
manifest_digest: binding.manifest_digest,
|
|
44
|
+
...(revision.deploymentId ? { deployment_id: revision.deploymentId } : {}),
|
|
45
|
+
}).toString();
|
|
46
|
+
const response = await fetcher(endpoint, {
|
|
47
|
+
redirect: "error",
|
|
48
|
+
cache: "no-store",
|
|
49
|
+
signal: AbortSignal.timeout(4000),
|
|
50
|
+
});
|
|
51
|
+
const body = (await response.json());
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw apiErrorFrom(response.status, body, "browser_auth_revision_unavailable");
|
|
54
|
+
}
|
|
55
|
+
if (typeof body.active !== "boolean")
|
|
56
|
+
throw new Error("revision_status_invalid");
|
|
57
|
+
if (body.valid_until !== undefined &&
|
|
58
|
+
(typeof body.valid_until !== "number" || !Number.isFinite(body.valid_until))) {
|
|
59
|
+
throw new Error("revision_status_invalid");
|
|
60
|
+
}
|
|
61
|
+
if (body.compatible_manifest_digest !== undefined &&
|
|
62
|
+
(typeof body.compatible_manifest_digest !== "string" ||
|
|
63
|
+
!body.compatible_manifest_digest ||
|
|
64
|
+
typeof body.valid_until !== "number" ||
|
|
65
|
+
!Number.isFinite(body.valid_until))) {
|
|
66
|
+
throw new Error("revision_status_invalid");
|
|
67
|
+
}
|
|
68
|
+
if (!body.active ||
|
|
69
|
+
(typeof body.valid_until === "number" && body.valid_until <= Date.now())) {
|
|
70
|
+
throw deploymentRefreshRequired();
|
|
71
|
+
}
|
|
72
|
+
if (Date.now() - started >= 4000)
|
|
73
|
+
throw new Error("revision_status_expired");
|
|
74
|
+
return {
|
|
75
|
+
...(typeof body.compatible_manifest_digest === "string"
|
|
76
|
+
? { compatibleManifestDigest: body.compatible_manifest_digest }
|
|
77
|
+
: {}),
|
|
78
|
+
...(typeof body.valid_until === "number" ? { validUntil: body.valid_until } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (cause) {
|
|
82
|
+
if (cause instanceof SegmentStreamApiError)
|
|
83
|
+
throw cause;
|
|
84
|
+
throw new SegmentStreamApiError({
|
|
85
|
+
code: "browser_auth_revision_unavailable",
|
|
86
|
+
status: 503,
|
|
87
|
+
detail: "browser_auth_revision_unavailable",
|
|
88
|
+
retryable: true,
|
|
89
|
+
cause,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
pending = check;
|
|
94
|
+
const forget = () => {
|
|
95
|
+
if (pending === check)
|
|
96
|
+
pending = undefined;
|
|
97
|
+
};
|
|
98
|
+
void check.then(forget, forget);
|
|
99
|
+
}
|
|
100
|
+
return pending;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Reconcile only with the exact live manifest Cloud admitted for this immutable client. */
|
|
104
|
+
export function createBrowserConfigurationCheck(fetcher, binding, revision, ontologyUrl) {
|
|
105
|
+
const admitted = createBrowserRevisionCheck(fetcher, binding, revision);
|
|
106
|
+
return async () => {
|
|
107
|
+
const admission = await admitted();
|
|
108
|
+
let response;
|
|
109
|
+
let live;
|
|
110
|
+
try {
|
|
111
|
+
response = await fetcher(`${await ontologyOrigin(ontologyUrl)}/api/ontology/federation/configuration`, { cache: "no-store", redirect: "error", signal: AbortSignal.timeout(10000) });
|
|
112
|
+
live = (await response.json());
|
|
113
|
+
}
|
|
114
|
+
catch (cause) {
|
|
115
|
+
throw new SegmentStreamApiError({
|
|
116
|
+
code: "browser_auth_configuration_unavailable",
|
|
117
|
+
status: 503,
|
|
118
|
+
detail: "browser_auth_configuration_unavailable",
|
|
119
|
+
retryable: true,
|
|
120
|
+
cause,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw apiErrorFrom(response.status, live, "browser_auth_configuration_unavailable");
|
|
125
|
+
}
|
|
126
|
+
if ((admission.validUntil !== undefined && admission.validUntil <= Date.now()) ||
|
|
127
|
+
live?.version !== binding.version ||
|
|
128
|
+
typeof live.manifest_digest !== "string" ||
|
|
129
|
+
!live.manifest_digest ||
|
|
130
|
+
(live.manifest_digest !== binding.manifest_digest &&
|
|
131
|
+
(admission.compatibleManifestDigest === undefined ||
|
|
132
|
+
live.manifest_digest !== admission.compatibleManifestDigest)) ||
|
|
133
|
+
live.browser?.primary !== binding.browser.primary ||
|
|
134
|
+
live.browser?.auto_redirect !== binding.browser.auto_redirect ||
|
|
135
|
+
JSON.stringify(live.browser?.methods) !== JSON.stringify(binding.browser.methods) ||
|
|
136
|
+
live.browser?.local?.port !== binding.browser.local?.port) {
|
|
137
|
+
throw deploymentRefreshRequired();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Browser-safe wire protocol shared by the ontology service and app SDK. */
|
|
2
|
+
export declare const CHANNEL_SESSION_PATH = "/api/ontology/session";
|
|
3
|
+
export declare const CHANNEL_LOGOUT_PATH = "/api/ontology/sign-in/logout";
|
|
4
|
+
export declare const CHANNEL_SESSION_TTL_SECONDS = 3600;
|
|
5
|
+
export declare const channelSignInPath: (provider: string) => string;
|
|
6
|
+
export declare const channelClientId: (provider: string) => string;
|
|
7
|
+
export declare const channelCookieName: (workspaceId: string) => string;
|
|
8
|
+
export declare const CHANNEL_COOKIE_PATTERN: RegExp;
|
|
9
|
+
export declare const channelCookieAttributes: {
|
|
10
|
+
httpOnly: boolean;
|
|
11
|
+
secure: boolean;
|
|
12
|
+
sameSite: "none";
|
|
13
|
+
partitioned: boolean;
|
|
14
|
+
path: string;
|
|
15
|
+
};
|
|
16
|
+
export declare const channelCookie: (name: string, token: string, maxAge: number) => string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Generated from packages/ontology-sdk/src/channel-session.ts; do not edit.
|
|
2
|
+
/** Browser-safe wire protocol shared by the ontology service and app SDK. */
|
|
3
|
+
export const CHANNEL_SESSION_PATH = "/api/ontology/session";
|
|
4
|
+
export const CHANNEL_LOGOUT_PATH = "/api/ontology/sign-in/logout";
|
|
5
|
+
export const CHANNEL_SESSION_TTL_SECONDS = 3600;
|
|
6
|
+
export const channelSignInPath = (provider) => `/api/ontology/sign-in/${encodeURIComponent(provider)}`;
|
|
7
|
+
export const channelClientId = (provider) => `${provider}-mini-app`;
|
|
8
|
+
export const channelCookieName = (workspaceId) => `__Host-ss_channel_session.${workspaceId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
9
|
+
export const CHANNEL_COOKIE_PATTERN = /^__Host-ss_channel_session\.[A-Za-z0-9_-]+$/;
|
|
10
|
+
export const channelCookieAttributes = {
|
|
11
|
+
httpOnly: true,
|
|
12
|
+
secure: true,
|
|
13
|
+
sameSite: "none",
|
|
14
|
+
partitioned: true,
|
|
15
|
+
path: "/",
|
|
16
|
+
};
|
|
17
|
+
export const channelCookie = (name, token, maxAge) => `${name}=${token}; HttpOnly; Secure; SameSite=None; Partitioned; Path=/; Max-Age=${maxAge}`;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import type { NextRequest } from "next/server";
|
|
3
|
+
import { type CurrentUser } from "./me.js";
|
|
4
|
+
import type { Fetch } from "./sessions.js";
|
|
5
|
+
export { channelCookieName } from "./channel-protocol.js";
|
|
6
|
+
export interface ChannelSessionIdentity {
|
|
7
|
+
readonly token: string;
|
|
8
|
+
readonly user: CurrentUser;
|
|
9
|
+
}
|
|
10
|
+
/** Workspace-owned opaque app sessions; every discovery checks current membership and revocation. */
|
|
11
|
+
export declare function createChannelSession(options: {
|
|
12
|
+
readonly enabled?: boolean;
|
|
13
|
+
readonly workspaceId: string;
|
|
14
|
+
readonly ontologyUrl: string | undefined;
|
|
15
|
+
readonly fetch: Fetch;
|
|
16
|
+
}): {
|
|
17
|
+
token: () => Promise<string | null>;
|
|
18
|
+
transport: import("./ontology.js").OntologyTransport;
|
|
19
|
+
read: () => Promise<ChannelSessionIdentity | null>;
|
|
20
|
+
signOut: () => Promise<void>;
|
|
21
|
+
proxy: (request: NextRequest) => Promise<NextResponse | undefined>;
|
|
22
|
+
};
|