@segmentstream-cloud/app 0.1.0 → 0.3.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 +230 -47
- package/dist/app.d.ts +42 -5
- package/dist/app.js +81 -7
- package/dist/config.d.ts +17 -1
- package/dist/config.js +29 -7
- package/dist/cookies.d.ts +16 -7
- package/dist/cookies.js +36 -47
- package/dist/data.d.ts +126 -0
- package/dist/data.js +206 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +66 -0
- package/dist/handlers.js +43 -19
- package/dist/index.d.ts +21 -7
- package/dist/index.js +12 -5
- package/dist/me.d.ts +24 -0
- package/dist/me.js +33 -0
- package/dist/neon.d.ts +92 -0
- package/dist/neon.js +414 -0
- package/dist/ontology-transport.d.ts +23 -0
- package/dist/ontology-transport.js +95 -0
- package/dist/ontology.d.ts +217 -0
- package/dist/ontology.js +141 -0
- package/dist/parameters.d.ts +55 -0
- package/dist/parameters.js +118 -0
- package/dist/permissions.d.ts +17 -0
- package/dist/permissions.js +33 -0
- package/dist/proxy.d.ts +2 -0
- package/dist/proxy.js +29 -13
- package/dist/return-to.d.ts +2 -0
- package/dist/return-to.js +24 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +35 -5
- package/dist/sign-in.d.ts +5 -1
- package/dist/sign-in.js +17 -4
- package/dist/tokens.d.ts +22 -3
- package/dist/ui/components.d.ts +27 -0
- package/dist/ui/components.js +63 -0
- package/dist/ui/hooks.d.ts +19 -0
- package/dist/ui/hooks.js +56 -0
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.js +8 -0
- package/dist/ui/navigation.d.ts +5 -0
- package/dist/ui/navigation.js +8 -0
- package/dist/ui/sign-in.d.ts +19 -0
- package/dist/ui/sign-in.js +198 -0
- package/dist/ui/store.d.ts +61 -0
- package/dist/ui/store.js +155 -0
- package/dist/ui/styles.css +249 -0
- package/package.json +24 -7
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# @segmentstream-cloud/app
|
|
2
2
|
|
|
3
|
-
Sign-in
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Sign-in, users, permissions, the ontology client and warehouse data access for
|
|
4
|
+
SegmentStream workspace apps built with Next.js (App Router, version 16 or later) and
|
|
5
|
+
React 19. The app's users sign in with an email code through Neon Auth (Managed
|
|
6
|
+
Better Auth) on the workspace's Neon project; the workspace's ontology decides who may
|
|
7
|
+
use the app and what they may do. The SDK wraps `@neondatabase/auth`, so the app never
|
|
8
|
+
configures Neon Auth itself.
|
|
6
9
|
|
|
7
10
|
## Install
|
|
8
11
|
|
|
@@ -10,17 +13,18 @@ members of the app's workspace get in.
|
|
|
10
13
|
npm install @segmentstream-cloud/app
|
|
11
14
|
```
|
|
12
15
|
|
|
13
|
-
##
|
|
16
|
+
## Set up
|
|
14
17
|
|
|
15
|
-
**`src/segmentstream.ts`** configures the app once
|
|
18
|
+
**`src/segmentstream.ts`** configures the app once. Pass the type of the app's
|
|
19
|
+
ontology to type `segmentstream.ontology`; the ontology is imported as a type only:
|
|
16
20
|
|
|
17
21
|
```ts
|
|
18
22
|
import { createSegmentStreamApp } from "@segmentstream-cloud/app";
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
import type ontology from "../../ontology";
|
|
25
|
+
import link from "../segmentstream.json"; // { "workspaceId": "…", "organizationId": "…" }
|
|
26
|
+
|
|
27
|
+
export const segmentstream = createSegmentStreamApp<typeof ontology>(link);
|
|
24
28
|
```
|
|
25
29
|
|
|
26
30
|
**`src/proxy.ts`** keeps every page and API route signed in:
|
|
@@ -29,22 +33,49 @@ export const segmentstream = createSegmentStreamApp({
|
|
|
29
33
|
import { segmentstream } from "./segmentstream";
|
|
30
34
|
|
|
31
35
|
export const proxy = segmentstream.proxy({
|
|
32
|
-
// Optional: paths
|
|
33
|
-
publicPaths: ["/
|
|
36
|
+
// Optional: paths anyone may open. `/webhooks` also covers `/webhooks/stripe`.
|
|
37
|
+
publicPaths: ["/webhooks"],
|
|
34
38
|
});
|
|
35
39
|
|
|
36
40
|
export const config = {
|
|
37
|
-
matcher: ["/((?!_next/static|_next/image|favicon.ico
|
|
41
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
38
42
|
};
|
|
39
43
|
```
|
|
40
44
|
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
Static assets, `/api/auth`, `/sign-in` and `/api/health` are always open.
|
|
46
|
+
|
|
47
|
+
**`src/app/api/auth/[...path]/route.ts`** serves the Neon Auth API that sign-in and
|
|
48
|
+
sessions use:
|
|
43
49
|
|
|
44
50
|
```ts
|
|
45
|
-
import { segmentstream } from "
|
|
51
|
+
import { segmentstream } from "@/segmentstream";
|
|
52
|
+
|
|
53
|
+
export const { GET, POST, PUT, DELETE, PATCH } = segmentstream.authHandlers();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**`src/app/sign-in/page.tsx`** renders the sign-in form, and the root layout imports
|
|
57
|
+
its stylesheet once:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { SignIn } from "@segmentstream-cloud/app/ui";
|
|
61
|
+
|
|
62
|
+
type SearchParams = Promise<Record<string, string | string[] | undefined>>;
|
|
63
|
+
|
|
64
|
+
export default async function SignInPage({ searchParams }: { searchParams: SearchParams }) {
|
|
65
|
+
const { returnTo, error } = await searchParams;
|
|
46
66
|
|
|
47
|
-
|
|
67
|
+
return (
|
|
68
|
+
<SignIn
|
|
69
|
+
returnTo={typeof returnTo === "string" ? returnTo : undefined}
|
|
70
|
+
error={typeof error === "string" ? error : undefined}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// src/app/layout.tsx
|
|
78
|
+
import "@segmentstream-cloud/app/ui/styles.css";
|
|
48
79
|
```
|
|
49
80
|
|
|
50
81
|
## Read the signed-in user
|
|
@@ -55,58 +86,210 @@ import { segmentstream } from "@/segmentstream";
|
|
|
55
86
|
export default async function Page() {
|
|
56
87
|
const user = await segmentstream.requireUser();
|
|
57
88
|
|
|
58
|
-
return
|
|
89
|
+
return (
|
|
90
|
+
<p>
|
|
91
|
+
{user.name ?? user.email}: {user.roles.join(", ")}
|
|
92
|
+
</p>
|
|
93
|
+
);
|
|
59
94
|
}
|
|
60
95
|
```
|
|
61
96
|
|
|
62
|
-
- `
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
97
|
+
- `requireUser()` returns `{ id, email?, name?, roles, permissions, kind?, active?,
|
|
98
|
+
authUserId?, organizationId, workspaceId }`. `id` is the person's ontology `user`
|
|
99
|
+
record. Roles and permissions come from the ontology's `GET /api/ontology/me`,
|
|
100
|
+
cached per session for a minute. Signed out, it redirects to `/sign-in`. For a
|
|
101
|
+
person the app has not invited (`403 not_invited`), it redirects to
|
|
102
|
+
`/sign-in?error=not_invited`, where `<SignIn />` asks them to request an invitation
|
|
103
|
+
or use another email.
|
|
104
|
+
- `getUser()` returns the same, or `null` when signed out; it throws
|
|
105
|
+
`SegmentStreamApiError` with `code: "not_invited"` for people without an invitation.
|
|
106
|
+
- `can("lead:changeStatus")` is `true` when the user's permissions cover the
|
|
107
|
+
permission. Roles may hold wildcards: `lead:*`, `*:read`, `*:*`. `isAllowed` and
|
|
108
|
+
`grants` apply the same rule to any list of permissions.
|
|
109
|
+
- `signOut()` ends the session, clears its cookies and redirects to `/sign-in` (or
|
|
110
|
+
`{ redirectTo }`). Call it from a Server Function or Route Handler.
|
|
111
|
+
|
|
112
|
+
Use them in Server Components, Route Handlers and Server Functions. Always check the
|
|
70
113
|
user in Server Functions and Route Handlers too; do not rely on the proxy alone.
|
|
71
114
|
|
|
72
|
-
|
|
115
|
+
## Client components
|
|
116
|
+
|
|
117
|
+
`@segmentstream-cloud/app/ui` holds React client components with plain, neutral CSS
|
|
118
|
+
(no Tailwind). Every class starts with `ss-`; the `--ss-*` custom properties on
|
|
119
|
+
`:root` set colours, radius and font.
|
|
120
|
+
|
|
121
|
+
| Export | Does |
|
|
122
|
+
| -------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
123
|
+
| `<SignIn />` | Email, then the six-digit code; resend after 30 seconds, errors and loading states |
|
|
124
|
+
| `<SignedIn>`, `<SignedOut>` | Render children by session state |
|
|
125
|
+
| `<Can permission="lead:changeStatus">` | Renders children when the user holds the permission; `fallback` otherwise |
|
|
126
|
+
| `<UserMenu />` | Name, email, roles and sign-out |
|
|
127
|
+
| `useUser()` | `{ status, user, error }` from `/api/ontology/me`, loaded once per session |
|
|
128
|
+
| `usePermissions()` | `{ isLoading, roles, permissions, can(permission) }` |
|
|
129
|
+
| `useSession()`, `signOut()` | The Neon Auth session in the browser; sign-out from the browser |
|
|
130
|
+
|
|
131
|
+
`<Can>` only hides controls: the ontology service checks every read and action. A
|
|
132
|
+
Server Component may wrap server-rendered children in `<Can>`:
|
|
73
133
|
|
|
74
134
|
```tsx
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
</
|
|
135
|
+
<Can permission="lead:changeStatus" fallback={lead.status}>
|
|
136
|
+
<ChangeStatusForm lead={lead.id} />
|
|
137
|
+
</Can>
|
|
78
138
|
```
|
|
79
139
|
|
|
140
|
+
## The ontology
|
|
141
|
+
|
|
142
|
+
`segmentstream.ontology.<entity>` calls the app's ontology service at `/api/ontology`
|
|
143
|
+
with the signed-in user's Neon Auth JWT, so the service applies their permissions.
|
|
144
|
+
Records, action inputs and action results are typed from the ontology:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const { items, nextCursor } = await segmentstream.ontology.lead.find({
|
|
148
|
+
where: { status: { in: ["new", "contacted"] } },
|
|
149
|
+
orderBy: [{ field: "createdAt", direction: "desc" }],
|
|
150
|
+
include: { owner: true },
|
|
151
|
+
limit: 20,
|
|
152
|
+
});
|
|
153
|
+
const lead = await segmentstream.ontology.lead.get(id);
|
|
154
|
+
const history = await segmentstream.ontology.lead.history(id, { limit: 50 });
|
|
155
|
+
const page = await segmentstream.ontology.lead.query({ where: { email: { startsWith: "ada" } } });
|
|
156
|
+
|
|
157
|
+
const input = { lead: id, status: "contacted" } as const;
|
|
158
|
+
const check = await segmentstream.ontology.lead.actions.changeStatus(input, { validateOnly: true });
|
|
159
|
+
const { result, validation, edits } = await segmentstream.ontology.lead.actions.changeStatus(input);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- `find` sends the query as URL parameters, `query` as a JSON body; both return
|
|
163
|
+
`{ items, nextCursor }`. Pass `nextCursor` as `after` for the next page.
|
|
164
|
+
- `actions.<name>(input, { validateOnly })` posts `{ input, options }` and returns
|
|
165
|
+
`{ result, validation, edits }`. Generated actions are there too
|
|
166
|
+
(`actions.create`, `update`, `delete`), and `action(name, input)` runs any action by
|
|
167
|
+
name, such as `upsertByEmail`.
|
|
168
|
+
- Failures throw `SegmentStreamApiError` with the service's `code` (`forbidden`,
|
|
169
|
+
`not_found`, `invalid_input`, `action_rejected`, `conflict`, `not_invited`), `status`,
|
|
170
|
+
`detail`, `requestId` and, for invalid input, `issues`.
|
|
171
|
+
- The types are read from the ontology SDK's `Ontology`, entity and action types
|
|
172
|
+
structurally, so the app needs neither the ontology SDK nor Effect at runtime.
|
|
173
|
+
Without a type argument, any entity name is accepted and records are untyped.
|
|
174
|
+
|
|
175
|
+
## Query the warehouse
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
import { sql } from "@segmentstream-cloud/app";
|
|
179
|
+
import { segmentstream } from "@/segmentstream";
|
|
180
|
+
|
|
181
|
+
type Revenue = { order_date: string; total_revenue: number };
|
|
182
|
+
|
|
183
|
+
export default async function Page() {
|
|
184
|
+
await segmentstream.requireUser();
|
|
185
|
+
|
|
186
|
+
const { rows } = await segmentstream.warehouse.query<Revenue>(
|
|
187
|
+
`SELECT order_date, total_revenue
|
|
188
|
+
FROM gold.revenue_summary
|
|
189
|
+
WHERE order_date >= @from
|
|
190
|
+
ORDER BY order_date`,
|
|
191
|
+
{ params: { from: sql.date("2026-09-01") } },
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return <pre>{JSON.stringify(rows, null, 2)}</pre>;
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- Warehouse calls authenticate with the workspace app credential,
|
|
199
|
+
`SEGMENTSTREAM_APP_CREDENTIAL`, which SegmentStream sets on deploy and the SDK reads
|
|
200
|
+
on every call. Without it they use the legacy sign-in's session token. The
|
|
201
|
+
credential belongs to the app, so check the user before querying for them.
|
|
202
|
+
- Queries are read-only GoogleSQL over the workspace warehouse. `dataset.table` resolves
|
|
203
|
+
to the workspace's project. Anything other than a single `SELECT` is rejected.
|
|
204
|
+
- Pass values as named `@parameters` in `params`; never build SQL from user input.
|
|
205
|
+
Strings, booleans, `bigint`, safe integers (`INT64`), other numbers (`FLOAT64`), `Date`
|
|
206
|
+
(`TIMESTAMP`) and arrays of these are typed automatically. Use `sql.date()`,
|
|
207
|
+
`sql.datetime()`, `sql.timestamp()`, `sql.numeric()`, `sql.bignumeric()`, `sql.int64()`
|
|
208
|
+
and `sql.array(type, values)` for anything else.
|
|
209
|
+
- `query()` waits for running queries (`timeoutMs`, default 60 s) and follows result
|
|
210
|
+
pages up to `maxRows` (default 10,000); `truncated` reports whether more rows exist.
|
|
211
|
+
`maximumBytesBilled` lowers the per-query byte ceiling; `warehouse.dryRun()` returns the
|
|
212
|
+
estimated bytes and result columns without running the query.
|
|
213
|
+
- Long queries keep running in the warehouse for up to an hour after `query()` stops
|
|
214
|
+
waiting. It then throws `query_timeout` with a `queryId`. Resume the query from a later
|
|
215
|
+
request, for example a Route Handler your page polls, with `warehouse.result(queryId)`.
|
|
216
|
+
Don't hold one request open for minutes.
|
|
217
|
+
- Values arrive as JSON: safe integers as numbers, larger integers and decimals as strings,
|
|
218
|
+
timestamps as ISO strings, records and arrays as JSON strings.
|
|
219
|
+
|
|
220
|
+
`segmentstream.semantic` (governed metrics from semantic models) and
|
|
221
|
+
`segmentstream.fetch(path)` (any SegmentStream API path) act as the signed-in
|
|
222
|
+
SegmentStream member, so they need the legacy sign-in; under Neon Auth they throw
|
|
223
|
+
`app_credential_missing`.
|
|
224
|
+
|
|
80
225
|
## How requests are handled
|
|
81
226
|
|
|
82
|
-
- Page navigations without a session are redirected to
|
|
83
|
-
return to the page
|
|
84
|
-
- API routes, `fetch` calls, non-GET requests
|
|
227
|
+
- Page navigations without a session are redirected to `/sign-in?returnTo=<path>` and
|
|
228
|
+
return to the page after sign-in.
|
|
229
|
+
- API routes, `fetch` calls, non-GET requests and Next.js data requests receive
|
|
85
230
|
`401 {"error":"unauthenticated"}` instead of a redirect.
|
|
86
|
-
- `/api/
|
|
231
|
+
- Static assets, `/api/auth/*`, `/sign-in`, `/api/health` and your `publicPaths` never
|
|
232
|
+
require a session.
|
|
87
233
|
|
|
88
234
|
## Environment
|
|
89
235
|
|
|
90
|
-
| Variable
|
|
91
|
-
|
|
|
92
|
-
| `
|
|
236
|
+
| Variable | Purpose |
|
|
237
|
+
| -------------------------------------- | ------------------------------------------------------------------------------- |
|
|
238
|
+
| `NEON_AUTH_BASE_URL` | The workspace's Neon Auth URL. Set: sign in with Neon Auth. Unset: 0.2 sign-in. |
|
|
239
|
+
| `SEGMENTSTREAM_APP_AUTH_COOKIE_SECRET` | 32 or more characters; signs the session cache cookie. Required with Neon Auth. |
|
|
240
|
+
| `SEGMENTSTREAM_APP_CREDENTIAL` | The workspace app credential for warehouse queries. |
|
|
241
|
+
| `SEGMENTSTREAM_ONTOLOGY_URL` | Optional: the ontology service when it does not run beside the app. |
|
|
242
|
+
| `SEGMENTSTREAM_URL` | SegmentStream origin. Defaults to `https://cloud.segmentstream.com`. |
|
|
243
|
+
|
|
244
|
+
SegmentStream sets the first three in the app's environment on deploy.
|
|
93
245
|
|
|
94
246
|
## Local development
|
|
95
247
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
248
|
+
Put the deployed `NEON_AUTH_BASE_URL` and a cookie secret in `app/.env.local`;
|
|
249
|
+
Neon Auth trusts `http://localhost:3000`:
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
NEON_AUTH_BASE_URL=https://ep-….neonauth.….aws.neon.tech/ontology/auth
|
|
253
|
+
SEGMENTSTREAM_APP_AUTH_COOKIE_SECRET=<openssl rand -base64 32>
|
|
254
|
+
SEGMENTSTREAM_ONTOLOGY_URL=http://localhost:3100
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
With `SEGMENTSTREAM_ONTOLOGY_URL`, server-side ontology calls go there and the proxy
|
|
258
|
+
forwards the browser's `/api/ontology/*` requests to it. Point it at a local
|
|
259
|
+
`npm --prefix ontology run dev` or at the deployed app.
|
|
99
260
|
|
|
100
261
|
## Security
|
|
101
262
|
|
|
102
|
-
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
263
|
+
- Sessions are Neon Auth's: `HttpOnly`, `Secure`, `SameSite=Lax` cookies on the app's
|
|
264
|
+
own domain, set through the app's `/api/auth` route. The session cache cookie is
|
|
265
|
+
signed with `SEGMENTSTREAM_APP_AUTH_COOKIE_SECRET`.
|
|
266
|
+
- Signing in proves an email address and grants nothing. The ontology links the
|
|
267
|
+
person to an invited member, and their roles decide what they may read and do. The
|
|
268
|
+
ontology service verifies the user's JWT (EdDSA, 15 minutes) and checks permissions
|
|
269
|
+
on every request; `can()` and `<Can>` only decide what to show.
|
|
270
|
+
- The session JWT and the app credential stay on the server, except that the browser
|
|
271
|
+
components fetch the JWT from the app's own `/api/auth/token` to read
|
|
272
|
+
`/api/ontology/me`.
|
|
273
|
+
- Warehouse queries run with the workspace's own identity; the app never holds
|
|
274
|
+
warehouse credentials.
|
|
275
|
+
|
|
276
|
+
## Without Neon Auth
|
|
277
|
+
|
|
278
|
+
Without `NEON_AUTH_BASE_URL`, 0.3 behaves exactly as 0.2, so existing apps keep
|
|
279
|
+
working: members of the workspace's organization sign in with their SegmentStream
|
|
280
|
+
account through `/api/segmentstream/[...path]/route.ts`
|
|
281
|
+
(`export const { GET, POST } = segmentstream.handlers();`), the proxy redirects to
|
|
282
|
+
SegmentStream, and `fetch`, `warehouse` and `semantic` act as the member. In that mode
|
|
283
|
+
`roles` and `permissions` are empty, `can()` is `false`, `authHandlers()` answers
|
|
284
|
+
`404 neon_auth_not_configured`, and `segmentstream.ontology` sends the member's app
|
|
285
|
+
token, which the ontology service accepts while workspaces move to Neon Auth. Keep the
|
|
286
|
+
legacy route until every deployment of the app has Neon Auth.
|
|
287
|
+
|
|
288
|
+
## Upgrading from 0.2
|
|
289
|
+
|
|
290
|
+
Add the auth route, the sign-in page and the stylesheet import; keep the legacy route.
|
|
291
|
+
`AppUser` gained `roles` and `permissions`. Under Neon Auth, people sign in again with
|
|
292
|
+
an email code, and the ontology must have invited them.
|
|
110
293
|
|
|
111
294
|
## License
|
|
112
295
|
|
package/dist/app.d.ts
CHANGED
|
@@ -1,23 +1,60 @@
|
|
|
1
|
-
import type { SegmentStreamAppConfig } from "./config.js";
|
|
1
|
+
import type { Environment, SegmentStreamAppConfig } from "./config.js";
|
|
2
|
+
import type { SemanticClient, WarehouseClient } from "./data.js";
|
|
2
3
|
import type { SegmentStreamRouteHandlers } from "./handlers.js";
|
|
4
|
+
import type { AuthRouteHandlers, CreateNeonAuth, SignOutOptions } from "./neon.js";
|
|
5
|
+
import type { OntologyClient } from "./ontology.js";
|
|
3
6
|
import type { ProxyOptions, SegmentStreamProxy } from "./proxy.js";
|
|
4
7
|
import type { Fetch } from "./sessions.js";
|
|
5
8
|
import type { AppUser, KeySet } from "./tokens.js";
|
|
6
|
-
|
|
9
|
+
/** How the app signs its users in: Neon Auth when `NEON_AUTH_BASE_URL` is set, else SegmentStream. */
|
|
10
|
+
export type AuthenticationMethod = "neon-auth" | "segmentstream";
|
|
11
|
+
/**
|
|
12
|
+
* The app's SegmentStream integration. `O` is the type of the app's ontology,
|
|
13
|
+
* `typeof import("../../ontology").default`, which types {@link SegmentStreamApp.ontology}.
|
|
14
|
+
*/
|
|
15
|
+
export interface SegmentStreamApp<O = unknown> {
|
|
16
|
+
/** `neon-auth` when `NEON_AUTH_BASE_URL` is set; `segmentstream` (the 0.2 sign-in) otherwise. */
|
|
17
|
+
readonly authentication: AuthenticationMethod;
|
|
7
18
|
/** The Next.js Proxy that keeps every non-public request signed in. */
|
|
8
19
|
readonly proxy: (options?: ProxyOptions) => SegmentStreamProxy;
|
|
9
|
-
/** Route Handlers for `app/api/
|
|
20
|
+
/** Route Handlers for `app/src/app/api/auth/[...path]/route.ts`: the Neon Auth API. */
|
|
21
|
+
readonly authHandlers: () => AuthRouteHandlers;
|
|
22
|
+
/** Route Handlers for `app/api/segmentstream/[...path]/route.ts`: the legacy sign-in. */
|
|
10
23
|
readonly handlers: () => SegmentStreamRouteHandlers;
|
|
11
24
|
/** The signed-in user for this request, or `null`. */
|
|
12
25
|
readonly getUser: () => Promise<AppUser | null>;
|
|
13
26
|
/** The signed-in user; redirects to sign-in when there is none. */
|
|
14
27
|
readonly requireUser: () => Promise<AppUser>;
|
|
15
|
-
/**
|
|
28
|
+
/** Whether the signed-in user's permissions cover `permission`, wildcards included. */
|
|
29
|
+
readonly can: (permission: string) => Promise<boolean>;
|
|
30
|
+
/**
|
|
31
|
+
* Ends the session and redirects to the sign-in page, or to `redirectTo`. Call it from a Server
|
|
32
|
+
* Function or Route Handler, which may change cookies.
|
|
33
|
+
*/
|
|
34
|
+
readonly signOut: (options?: SignOutOptions) => Promise<never>;
|
|
35
|
+
/** The verified legacy app session token for this request, or `null`; always `null` under Neon Auth. */
|
|
16
36
|
readonly getAccessToken: () => Promise<string | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Calls any SegmentStream API path as the signed-in member, with their organization role
|
|
39
|
+
* and permissions. Server-only; the session token never reaches the browser. Legacy sign-in only.
|
|
40
|
+
*/
|
|
41
|
+
readonly fetch: (path: string, init?: RequestInit) => Promise<Response>;
|
|
42
|
+
/**
|
|
43
|
+
* Read-only SQL over the workspace warehouse. Server-only. Uses the workspace app credential
|
|
44
|
+
* (`SEGMENTSTREAM_APP_CREDENTIAL`) when it is set, else the signed-in member's session.
|
|
45
|
+
*/
|
|
46
|
+
readonly warehouse: WarehouseClient;
|
|
47
|
+
/** Governed metrics from the workspace's deployed semantic models. Server-only. Legacy sign-in only. */
|
|
48
|
+
readonly semantic: SemanticClient;
|
|
49
|
+
/** The app's ontology service, `/api/ontology`, as the signed-in user. Server-only. */
|
|
50
|
+
readonly ontology: OntologyClient<O>;
|
|
17
51
|
}
|
|
18
52
|
/** Replaceable capabilities, for tests. */
|
|
19
53
|
export interface AppDependencies {
|
|
20
54
|
readonly fetch: Fetch;
|
|
21
55
|
readonly keySet: KeySet;
|
|
56
|
+
readonly environment: Environment;
|
|
57
|
+
readonly createNeonAuth: CreateNeonAuth;
|
|
58
|
+
readonly now: () => number;
|
|
22
59
|
}
|
|
23
|
-
export declare function createApp(config: SegmentStreamAppConfig, dependencies?: Partial<AppDependencies>): SegmentStreamApp
|
|
60
|
+
export declare function createApp<O = unknown>(config: SegmentStreamAppConfig, dependencies?: Partial<AppDependencies>): SegmentStreamApp<O>;
|
package/dist/app.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createNeonAuth } from "@neondatabase/auth/next/server";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import { environmentValue, processEnvironment, resolveConfig, resolveNeonAuth } from "./config.js";
|
|
4
|
+
import { createDataClient } from "./data.js";
|
|
5
|
+
import { SegmentStreamApiError } from "./errors.js";
|
|
2
6
|
import { createHandlers } from "./handlers.js";
|
|
7
|
+
import { createNeonSession } from "./neon.js";
|
|
8
|
+
import { createOntologyClient } from "./ontology.js";
|
|
9
|
+
import { createOntologyTransport } from "./ontology-transport.js";
|
|
3
10
|
import { createProxy } from "./proxy.js";
|
|
4
11
|
import { createServerSession } from "./server.js";
|
|
5
12
|
import { createSessionClient } from "./sessions.js";
|
|
6
13
|
import { createTokenVerifier, remoteKeySet } from "./tokens.js";
|
|
14
|
+
const NEON_AUTH_NOT_CONFIGURED = "This app signs in with SegmentStream: NEON_AUTH_BASE_URL is not set. SegmentStream sets it on deploy once the workspace has Neon Auth.";
|
|
7
15
|
export function createApp(config, dependencies = {}) {
|
|
8
|
-
const
|
|
16
|
+
const environment = dependencies.environment ?? processEnvironment();
|
|
17
|
+
const resolved = resolveConfig(config, environment);
|
|
18
|
+
const neonAuth = resolveNeonAuth(environment);
|
|
9
19
|
const keySet = dependencies.keySet ?? remoteKeySet(resolved.baseUrl);
|
|
10
20
|
const fetchImplementation = dependencies.fetch ?? ((input, init) => fetch(input, init));
|
|
11
21
|
const sessionClient = createSessionClient(resolved, fetchImplementation);
|
|
@@ -15,12 +25,76 @@ export function createApp(config, dependencies = {}) {
|
|
|
15
25
|
requestToken: sessionClient.requestToken,
|
|
16
26
|
revokeSession: sessionClient.revoke,
|
|
17
27
|
};
|
|
18
|
-
const
|
|
28
|
+
const legacy = createServerSession(context);
|
|
29
|
+
const getAppCredential = () => environmentValue(environment, "SEGMENTSTREAM_APP_CREDENTIAL");
|
|
30
|
+
if (neonAuth === null) {
|
|
31
|
+
const data = createDataClient({
|
|
32
|
+
config: resolved,
|
|
33
|
+
fetch: fetchImplementation,
|
|
34
|
+
getAccessToken: legacy.getAccessToken,
|
|
35
|
+
getAppCredential,
|
|
36
|
+
});
|
|
37
|
+
const ontologyUrl = environmentValue(environment, "SEGMENTSTREAM_ONTOLOGY_URL");
|
|
38
|
+
return {
|
|
39
|
+
authentication: "segmentstream",
|
|
40
|
+
proxy: (options) => createProxy(context, options),
|
|
41
|
+
authHandlers: notConfiguredHandlers,
|
|
42
|
+
handlers: () => createHandlers(context),
|
|
43
|
+
getUser: legacy.getUser,
|
|
44
|
+
requireUser: legacy.requireUser,
|
|
45
|
+
// Ontology permissions come with Neon Auth; the legacy sign-in grants none.
|
|
46
|
+
can: async () => false,
|
|
47
|
+
signOut: legacy.signOut,
|
|
48
|
+
getAccessToken: legacy.getAccessToken,
|
|
49
|
+
fetch: data.fetch,
|
|
50
|
+
warehouse: data.warehouse,
|
|
51
|
+
semantic: data.semantic,
|
|
52
|
+
// The ontology service accepts SegmentStream app tokens while workspaces move to Neon Auth.
|
|
53
|
+
ontology: createOntologyClient(createOntologyTransport({
|
|
54
|
+
fetch: fetchImplementation,
|
|
55
|
+
ontologyUrl: ontologyUrl === undefined ? undefined : new URL(ontologyUrl).origin,
|
|
56
|
+
credentials: { token: legacy.getAccessToken },
|
|
57
|
+
})),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const neon = createNeonSession({
|
|
61
|
+
config: resolved,
|
|
62
|
+
settings: neonAuth,
|
|
63
|
+
createNeonAuth: dependencies.createNeonAuth ?? createNeonAuth,
|
|
64
|
+
fetch: fetchImplementation,
|
|
65
|
+
now: dependencies.now ?? Date.now,
|
|
66
|
+
});
|
|
67
|
+
const data = createDataClient({
|
|
68
|
+
config: resolved,
|
|
69
|
+
fetch: fetchImplementation,
|
|
70
|
+
getAccessToken: noSegmentStreamSession,
|
|
71
|
+
getAppCredential,
|
|
72
|
+
});
|
|
19
73
|
return {
|
|
20
|
-
|
|
74
|
+
authentication: "neon-auth",
|
|
75
|
+
proxy: neon.proxy,
|
|
76
|
+
authHandlers: neon.authHandlers,
|
|
21
77
|
handlers: () => createHandlers(context),
|
|
22
|
-
getUser:
|
|
23
|
-
requireUser:
|
|
24
|
-
|
|
78
|
+
getUser: neon.getUser,
|
|
79
|
+
requireUser: neon.requireUser,
|
|
80
|
+
can: neon.can,
|
|
81
|
+
signOut: neon.signOut,
|
|
82
|
+
getAccessToken: async () => null,
|
|
83
|
+
fetch: data.fetch,
|
|
84
|
+
warehouse: data.warehouse,
|
|
85
|
+
semantic: data.semantic,
|
|
86
|
+
ontology: createOntologyClient(neon.ontologyTransport),
|
|
25
87
|
};
|
|
26
88
|
}
|
|
89
|
+
/** Under Neon Auth the app has no SegmentStream session to call the API with. */
|
|
90
|
+
async function noSegmentStreamSession() {
|
|
91
|
+
throw new SegmentStreamApiError({
|
|
92
|
+
code: "app_credential_missing",
|
|
93
|
+
status: 401,
|
|
94
|
+
detail: "Apps that sign in with Neon Auth have no SegmentStream session. Warehouse queries use the workspace app credential, SEGMENTSTREAM_APP_CREDENTIAL, which is not set; SegmentStream sets it on deploy. Other SegmentStream API calls are not available to these apps yet.",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function notConfiguredHandlers() {
|
|
98
|
+
const handler = async () => NextResponse.json({ error: "neon_auth_not_configured", detail: NEON_AUTH_NOT_CONFIGURED }, { status: 404, headers: { "cache-control": "no-store" } });
|
|
99
|
+
return { GET: handler, POST: handler, PUT: handler, DELETE: handler, PATCH: handler };
|
|
100
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -13,6 +13,22 @@ export interface ResolvedConfig {
|
|
|
13
13
|
readonly baseUrl: string;
|
|
14
14
|
readonly issuer: string;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
/** Environment variables, `process.env` unless a test supplies its own. */
|
|
17
|
+
export type Environment = Readonly<Record<string, string | undefined>>;
|
|
18
|
+
export declare function processEnvironment(): Environment;
|
|
19
|
+
/** Neon Auth for the app's users, configured when `NEON_AUTH_BASE_URL` is set. */
|
|
20
|
+
export interface NeonAuthEnvironment {
|
|
21
|
+
/** `NEON_AUTH_BASE_URL`: the branch's Auth URL. */
|
|
22
|
+
readonly baseUrl: string;
|
|
23
|
+
/** `SEGMENTSTREAM_APP_AUTH_COOKIE_SECRET`: signs the session cache cookie. */
|
|
24
|
+
readonly cookieSecret: string | undefined;
|
|
25
|
+
/** `SEGMENTSTREAM_ONTOLOGY_URL`: the ontology service when it is not beside the app. */
|
|
26
|
+
readonly ontologyUrl: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
export declare function resolveConfig(config: SegmentStreamAppConfig, environment?: Environment): ResolvedConfig;
|
|
29
|
+
/** `null` without `NEON_AUTH_BASE_URL`: the app then signs in with SegmentStream, as in 0.2. */
|
|
30
|
+
export declare function resolveNeonAuth(environment: Environment): NeonAuthEnvironment | null;
|
|
17
31
|
/** Linear-time trailing slash removal; `/\/+$/` backtracks quadratically on slash runs. */
|
|
18
32
|
export declare function trimTrailingSlashes(value: string): string;
|
|
33
|
+
/** A variable's trimmed value; unset and blank are the same. */
|
|
34
|
+
export declare function environmentValue(environment: Environment, name: string): string | undefined;
|
package/dist/config.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
export const DEFAULT_BASE_URL = "https://cloud.segmentstream.com";
|
|
2
|
-
export function
|
|
2
|
+
export function processEnvironment() {
|
|
3
|
+
return typeof process === "undefined" ? {} : process.env;
|
|
4
|
+
}
|
|
5
|
+
export function resolveConfig(config, environment = processEnvironment()) {
|
|
3
6
|
const workspaceId = requireIdentifier("workspaceId", config.workspaceId);
|
|
4
7
|
const organizationId = requireIdentifier("organizationId", config.organizationId);
|
|
5
|
-
const baseUrl = normalizeBaseUrl(config.baseUrl ??
|
|
6
|
-
const issuer =
|
|
8
|
+
const baseUrl = normalizeBaseUrl(config.baseUrl ?? environmentValue(environment, "SEGMENTSTREAM_URL") ?? DEFAULT_BASE_URL);
|
|
9
|
+
const issuer = environmentValue(environment, "SEGMENTSTREAM_APP_TOKEN_ISSUER") ?? new URL(baseUrl).origin;
|
|
7
10
|
return { workspaceId, organizationId, baseUrl, issuer };
|
|
8
11
|
}
|
|
12
|
+
/** `null` without `NEON_AUTH_BASE_URL`: the app then signs in with SegmentStream, as in 0.2. */
|
|
13
|
+
export function resolveNeonAuth(environment) {
|
|
14
|
+
const baseUrl = environmentValue(environment, "NEON_AUTH_BASE_URL");
|
|
15
|
+
if (baseUrl === undefined) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const ontologyUrl = environmentValue(environment, "SEGMENTSTREAM_ONTOLOGY_URL");
|
|
19
|
+
return {
|
|
20
|
+
baseUrl: normalizeUrl("NEON_AUTH_BASE_URL", baseUrl),
|
|
21
|
+
cookieSecret: environmentValue(environment, "SEGMENTSTREAM_APP_AUTH_COOKIE_SECRET"),
|
|
22
|
+
ontologyUrl: ontologyUrl === undefined
|
|
23
|
+
? undefined
|
|
24
|
+
: new URL(normalizeUrl("SEGMENTSTREAM_ONTOLOGY_URL", ontologyUrl)).origin,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
9
27
|
function requireIdentifier(name, value) {
|
|
10
28
|
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
11
29
|
if (trimmed === "") {
|
|
@@ -22,20 +40,24 @@ export function trimTrailingSlashes(value) {
|
|
|
22
40
|
return value.slice(0, end);
|
|
23
41
|
}
|
|
24
42
|
function normalizeBaseUrl(value) {
|
|
43
|
+
return normalizeUrl("SegmentStream base URL", value);
|
|
44
|
+
}
|
|
45
|
+
function normalizeUrl(name, value) {
|
|
25
46
|
const trimmed = trimTrailingSlashes(value.trim());
|
|
26
47
|
let url;
|
|
27
48
|
try {
|
|
28
49
|
url = new URL(trimmed);
|
|
29
50
|
}
|
|
30
51
|
catch (error) {
|
|
31
|
-
throw new Error(
|
|
52
|
+
throw new Error(`${name} is not a valid URL: ${trimmed}`, { cause: error });
|
|
32
53
|
}
|
|
33
54
|
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
34
|
-
throw new Error(
|
|
55
|
+
throw new Error(`${name} must use http or https: ${trimmed}`);
|
|
35
56
|
}
|
|
36
57
|
return trimmed;
|
|
37
58
|
}
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
/** A variable's trimmed value; unset and blank are the same. */
|
|
60
|
+
export function environmentValue(environment, name) {
|
|
61
|
+
const value = environment[name];
|
|
40
62
|
return value === undefined || value.trim() === "" ? undefined : value.trim();
|
|
41
63
|
}
|
package/dist/cookies.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { NextResponse } from "next/server";
|
|
2
|
+
export { safeReturnTo } from "./return-to.js";
|
|
2
3
|
export declare const REFRESH_COOKIE_MAX_AGE_SECONDS: number;
|
|
3
4
|
export declare const AUTH_FLOW_COOKIE_MAX_AGE_SECONDS: number;
|
|
5
|
+
/** Sign-ins one browser can have in progress at once; one more abandons them all. */
|
|
6
|
+
export declare const MAX_PENDING_AUTH_FLOWS = 4;
|
|
4
7
|
export interface CookieNames {
|
|
5
8
|
readonly session: string;
|
|
6
9
|
readonly refresh: string;
|
|
7
|
-
|
|
10
|
+
/** Each sign-in in progress has its own cookie: this prefix followed by its `state`. */
|
|
11
|
+
readonly authFlowPrefix: string;
|
|
8
12
|
}
|
|
9
|
-
/** Cookie names and attributes for one request origin. */
|
|
13
|
+
/** Cookie names and attributes for one app on one request origin. */
|
|
10
14
|
export interface CookiePolicy {
|
|
11
15
|
readonly secure: boolean;
|
|
12
16
|
readonly names: CookieNames;
|
|
@@ -28,18 +32,23 @@ export declare function isLoopbackHostname(hostname: string): boolean;
|
|
|
28
32
|
* Browsers reject `Secure` and `__Host-` cookies on plain-HTTP origins, except that
|
|
29
33
|
* local development on http://localhost should still work. Every other origin uses
|
|
30
34
|
* `__Host-` cookies, which cannot be set by sibling subdomains.
|
|
35
|
+
*
|
|
36
|
+
* Browsers share cookies between ports, so apps on `localhost:3000` and `localhost:3001`
|
|
37
|
+
* see one cookie jar. Names carry the workspace id to keep each app's session its own.
|
|
31
38
|
*/
|
|
32
|
-
export declare function cookiePolicyFor(url: URL): CookiePolicy;
|
|
39
|
+
export declare function cookiePolicyFor(url: URL, workspaceId: string): CookiePolicy;
|
|
33
40
|
/** Candidate cookie names for code that only knows the request host (Server Components). */
|
|
34
|
-
export declare function sessionCookieCandidates(hostname: string): readonly string[];
|
|
41
|
+
export declare function sessionCookieCandidates(hostname: string, workspaceId: string): readonly string[];
|
|
42
|
+
/** The cookie holding one sign-in's flow, or undefined for a `state` this SDK never issues. */
|
|
43
|
+
export declare function authFlowCookieName(policy: CookiePolicy, state: string): string | undefined;
|
|
44
|
+
/** The sign-in flow cookies a request carries, one per sign-in still in progress. */
|
|
45
|
+
export declare function pendingAuthFlowCookies(policy: CookiePolicy, requestCookieNames: readonly string[]): string[];
|
|
35
46
|
export declare function setSessionCookies(response: NextResponse, policy: CookiePolicy, tokens: SessionTokens): void;
|
|
36
47
|
export declare function clearSessionCookies(response: NextResponse, policy: CookiePolicy): void;
|
|
37
48
|
export declare function setAuthFlowCookie(response: NextResponse, policy: CookiePolicy, flow: AuthFlowState): void;
|
|
38
|
-
export declare function
|
|
49
|
+
export declare function clearCookie(response: NextResponse, policy: CookiePolicy, name: string): void;
|
|
39
50
|
export declare function encodeAuthFlowState(flow: AuthFlowState): string;
|
|
40
51
|
export declare function decodeAuthFlowState(value: string | undefined): AuthFlowState | null;
|
|
41
|
-
/** Reduces a return path to a same-origin relative path, falling back to `/`. */
|
|
42
|
-
export declare function safeReturnTo(value: string | null | undefined): string;
|
|
43
52
|
/**
|
|
44
53
|
* Replaces the named cookies in a raw `Cookie` request header while preserving every
|
|
45
54
|
* other cookie byte for byte.
|