@segmentstream/app 0.0.0-stage → 0.38.1
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/README.md
CHANGED
|
@@ -1,3 +1,1237 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @segmentstream/app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Releases up to the scope change were published as `@segmentstream-cloud/app`; versions continue
|
|
4
|
+
from them, with the same exports and API.
|
|
5
|
+
|
|
6
|
+
The ontology client uses camelCase entities, records and actions, including
|
|
7
|
+
`createdAt`, `assignedOwnerId`, and `actions.changeStatus(...)`. It translates
|
|
8
|
+
queries and declared action inputs to snake_case REST and decodes records back to
|
|
9
|
+
camelCase. The catalog is read lazily and cached by ontology hash so nested
|
|
10
|
+
relationships and select identifiers convert while JSON property contents,
|
|
11
|
+
provider payloads and opaque action results remain unchanged.
|
|
12
|
+
|
|
13
|
+
Sign-in, users, permissions, the ontology client and warehouse data access for
|
|
14
|
+
SegmentStream workspace apps built with Next.js (App Router, version 16.4.1 or later) and
|
|
15
|
+
React 19. Compatibility deployments sign in with an email code, or a sign-in link the first time,
|
|
16
|
+
through Neon Auth (Managed
|
|
17
|
+
Better Auth) on the workspace's Neon project; the workspace's ontology decides who may
|
|
18
|
+
use the app and what they may do. The SDK wraps `@neon/auth`, so the app never
|
|
19
|
+
configures Neon Auth itself.
|
|
20
|
+
|
|
21
|
+
## Declared browser methods
|
|
22
|
+
|
|
23
|
+
Workspace browser authentication is declared in `ontology/index.ts`:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
signIn: {
|
|
27
|
+
policy: "invited",
|
|
28
|
+
browser: { methods: ["segmentstream"], primary: "segmentstream", autoRedirect: true, local: { port: 3000 } },
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
New starters use this declaration. Existing workspaces that omit `browser` retain
|
|
33
|
+
Neon email authentication and optional configured SegmentStream login.
|
|
34
|
+
|
|
35
|
+
This first release allows additive adoption only. A first-ever deployment may
|
|
36
|
+
start with SegmentStream alone. An existing email workspace adopts
|
|
37
|
+
`methods: ["email", "segmentstream"], primary: "email", autoRedirect: false`.
|
|
38
|
+
Deployment rejects removing any currently serving browser method before migration;
|
|
39
|
+
email-to-SegmentStream-only cutover and dual-to-email rollback are unavailable.
|
|
40
|
+
A source rollback must retain the explicit declaration and its serving methods.
|
|
41
|
+
Full method removal requires a later staged-activation release; linking accounts
|
|
42
|
+
does not make method removal safe in this release.
|
|
43
|
+
|
|
44
|
+
Email possession never merges established users. `useIdentityLink()` confirms an
|
|
45
|
+
existing Neon workspace login before explicitly connecting a SegmentStream account;
|
|
46
|
+
that account may use a different email. `useEmailIdentityLink()` verifies and
|
|
47
|
+
explicitly attaches a Neon identity only when its email matches the canonical
|
|
48
|
+
workspace user email. A missing/different workspace email requires administrator
|
|
49
|
+
recovery. The SegmentStream account email is not a replacement for that profile.
|
|
50
|
+
Reverse linking preserves identity but does not enable method removal or an
|
|
51
|
+
email-only rollback in this release.
|
|
52
|
+
|
|
53
|
+
Start a deliberate Neon-to-SegmentStream connection with
|
|
54
|
+
`useBrowserSignIn().connectAccountUrl` (`GET /api/segmentstream/connect`), not the
|
|
55
|
+
ordinary `segmentstreamSignInUrl`. Connect requires an existing active Neon
|
|
56
|
+
workspace login. Its protected flow captures that user and sends fresh Neon proof
|
|
57
|
+
with `intent: "link"` during callback exchange, bypassing new-user enrollment even
|
|
58
|
+
under open admission. Switching the email workspace account aborts the connection.
|
|
59
|
+
|
|
60
|
+
`useIdentityLink()` reads the pending request's expiry and trusted SegmentStream
|
|
61
|
+
identity (`id`, optional `email`/`name`) together with the current workspace user's
|
|
62
|
+
`id`, optional `email`/`name`. Render both before a deliberate confirmation POST to
|
|
63
|
+
`confirmationUrl`; opening the page never links identities. Expired requests and
|
|
64
|
+
account changes fail closed. The ontology rechecks the exact verified Neon proof;
|
|
65
|
+
client-provided user IDs are never identity proof. The starter's `/sign-in/link`
|
|
66
|
+
page demonstrates this review, including different-email forward linking.
|
|
67
|
+
|
|
68
|
+
Deployment checks the side-effect-free
|
|
69
|
+
`@segmentstream/app/browser-auth-capability` export before accepting a
|
|
70
|
+
browser declaration. `app dev` also checks the installed app SDK from `app/`
|
|
71
|
+
before evaluating ontology source or making an authenticated request; install
|
|
72
|
+
capable app dependencies even when starting only the local ontology service. Its named `browserAuthCapability` value is a protocol
|
|
73
|
+
capability, not an SDK release version. Missing/unsupported capability fails the
|
|
74
|
+
build instead of allowing an older SDK to ignore the declaration.
|
|
75
|
+
|
|
76
|
+
Deployment derives `SEGMENTSTREAM_BROWSER_AUTH` from the manifest. Do not author a
|
|
77
|
+
second policy in app configuration. `useBrowserSignIn()` exposes enabled methods,
|
|
78
|
+
safe destinations, recovery error codes, and an account-switch URL; the workspace
|
|
79
|
+
supplies its own text and controls. Sole-method automatic login applies only to
|
|
80
|
+
normal GET page navigation. Error/recovery pages, APIs, prefetch and non-GET
|
|
81
|
+
requests never restart authentication automatically.
|
|
82
|
+
|
|
83
|
+
During deployment, Cloud may admit the immediately preceding production browser revision for
|
|
84
|
+
up to 30 seconds after confirmed promotion when its authentication, origin and relevant ontology
|
|
85
|
+
contracts remain compatible. Revision admission is checked on each protected request. The SDK
|
|
86
|
+
accepts a different configuration digest only when Cloud explicitly admits that exact live digest.
|
|
87
|
+
Disabled clients, removed methods and changed security policy never gain this overlap.
|
|
88
|
+
|
|
89
|
+
A page pinned to a revision that cannot continue receives `409 deployment_refresh_required`,
|
|
90
|
+
`retryable: false`, and an `x-segmentstream-error` response header. Server calls throw
|
|
91
|
+
`SegmentStreamApiError` with the same code. The workspace supplies its recovery wording and
|
|
92
|
+
refresh controls. The SDK never reloads these pages or replays a POST in response to this signal. Revision
|
|
93
|
+
status and configuration network failures retain `browser_auth_revision_unavailable` and
|
|
94
|
+
`browser_auth_configuration_unavailable`; authentication and session errors retain their own codes.
|
|
95
|
+
|
|
96
|
+
SegmentStream accounts and Cloud access are separate. An app-only member needs no
|
|
97
|
+
Cloud organization membership; app signup and invitations do not grant organization
|
|
98
|
+
creation. SegmentStream-only launch requires explicitly confirmed open WorkOS account
|
|
99
|
+
registration. Closed-registration invitation handoff is not implemented. An emailed
|
|
100
|
+
app link preserves its safe destination but may require interactive WorkOS login,
|
|
101
|
+
registration, verification, consent, or account recovery; it is not a universal
|
|
102
|
+
signed-out one-click login. Compatibility and dual deployments keep their Neon
|
|
103
|
+
email-link behavior and workspace-owned templates/reply routing.
|
|
104
|
+
|
|
105
|
+
For local development install app and ontology dependencies, supply an isolated
|
|
106
|
+
local ontology database, and keep the ontology dev service on loopback port 3100.
|
|
107
|
+
Run these from the workspace root in separate terminals:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
segmentstream app dev -- npm --prefix ontology run dev
|
|
111
|
+
segmentstream app dev -- npm --prefix app run dev
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`app dev` currently supports SegmentStream-only local source. It rejects dual-method
|
|
115
|
+
and email-only declarations before authentication, client registration, or launching
|
|
116
|
+
a dev process. Those workspaces keep their existing independently configured local
|
|
117
|
+
email workflow until isolated local Neon bindings are supported. Do not copy
|
|
118
|
+
production Neon credentials or silently change the deployed browser policy to use
|
|
119
|
+
this command. Deployed dual-method authentication remains supported.
|
|
120
|
+
|
|
121
|
+
The CLI evaluates the local ontology, registers an exact temporary loopback client
|
|
122
|
+
for its declared `local.port`, and passes matching configuration to each process.
|
|
123
|
+
Restart after changing the declaration or after the 24-hour client deadline. It
|
|
124
|
+
does not create a database or persist authentication configuration. The app must
|
|
125
|
+
listen on its declared port; this starter uses 3000. Cloud checks `app_auth:manage`
|
|
126
|
+
for client registration. Local activity does not count as production migration.
|
|
127
|
+
|
|
128
|
+
## Install
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
npm install @segmentstream/app
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Deploying workspace source
|
|
135
|
+
|
|
136
|
+
Main pushes deploy the workspace by default. To accumulate source updates for an explicit
|
|
137
|
+
release, commit `.segmentstream/deployment.json` with
|
|
138
|
+
`{ "schemaVersion": 1, "trigger": "manual" }`. Pushes still store and index source;
|
|
139
|
+
`segmentstream deploy` explicitly deploys current main with the existing authorization and
|
|
140
|
+
idempotency checks. Removing the file or selecting `"push"` restores automatic deployment.
|
|
141
|
+
This repository setting is read by Cloud, not by the app SDK, and adds no scheduler.
|
|
142
|
+
|
|
143
|
+
For an existing hourly release process, the optional `productionWindow` setting
|
|
144
|
+
in the same file restricts explicit production admission to configured UTC minutes.
|
|
145
|
+
Outside that window, create and retry return `deployment_window_closed` with the
|
|
146
|
+
next opening time; no attempt starts. This adds no scheduler or preview restriction.
|
|
147
|
+
|
|
148
|
+
## Set up
|
|
149
|
+
|
|
150
|
+
The proxy and server-client setup is shared. The auth-route/sign-in form example
|
|
151
|
+
below is for enabled email compatibility. Declared SegmentStream browser methods
|
|
152
|
+
use workspace-owned `useBrowserSignIn()` UI and `segmentstream.handlers()` at
|
|
153
|
+
`/api/segmentstream/[...path]`; the starter supplies those routes and recovery pages.
|
|
154
|
+
|
|
155
|
+
**`src/segmentstream.ts`** configures the app once. Pass the type of the app's
|
|
156
|
+
ontology to type `segmentstream.ontology`; the ontology is imported as a type only:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { createSegmentStreamApp } from "@segmentstream/app";
|
|
160
|
+
|
|
161
|
+
import type ontology from "../../ontology";
|
|
162
|
+
import link from "../segmentstream.json"; // { "workspaceId": "…", "organizationId": "…" }
|
|
163
|
+
|
|
164
|
+
export const segmentstream = createSegmentStreamApp<typeof ontology>(link);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**`src/proxy.ts`** keeps every page and API route signed in:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { segmentstream } from "./segmentstream";
|
|
171
|
+
|
|
172
|
+
export const proxy = segmentstream.proxy({
|
|
173
|
+
// Optional: paths anyone may open. `/webhooks` also covers `/webhooks/stripe`.
|
|
174
|
+
publicPaths: ["/webhooks"],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export const config = {
|
|
178
|
+
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
|
179
|
+
};
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Static assets, `/api/auth`, `/sign-in` and `/api/health` are always open.
|
|
183
|
+
|
|
184
|
+
**`src/app/api/auth/[...path]/route.ts`** serves the Neon Auth API that sign-in and
|
|
185
|
+
sessions use:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { segmentstream } from "@/segmentstream";
|
|
189
|
+
|
|
190
|
+
export const { GET, POST, PUT, DELETE, PATCH } = segmentstream.authHandlers();
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The route completes emailed sign-in links (`/api/auth/magic-link/verify`) itself: it
|
|
194
|
+
verifies the token through Neon Auth without a callback, sets the session cookies on
|
|
195
|
+
the app's domain and redirects to the link's `callbackURL` path. An expired, used or
|
|
196
|
+
unknown link redirects to `/sign-in?error=sign_in_link_invalid`, where `<SignIn />` (or
|
|
197
|
+
`useSignIn().linkInvalid`) offers a new one.
|
|
198
|
+
|
|
199
|
+
**`src/app/sign-in/page.tsx`** renders the sign-in form, and the root layout imports
|
|
200
|
+
its stylesheet once. Apps with their own design system render their own form on
|
|
201
|
+
[`useSignIn()`](#your-own-sign-in-form) instead and skip the stylesheet:
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
import { SignIn } from "@segmentstream/app/ui";
|
|
205
|
+
|
|
206
|
+
type SearchParams = Promise<Record<string, string | string[] | undefined>>;
|
|
207
|
+
|
|
208
|
+
export default async function SignInPage({ searchParams }: { searchParams: SearchParams }) {
|
|
209
|
+
const { returnTo, error } = await searchParams;
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<SignIn
|
|
213
|
+
returnTo={typeof returnTo === "string" ? returnTo : undefined}
|
|
214
|
+
error={typeof error === "string" ? error : undefined}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
// src/app/layout.tsx
|
|
222
|
+
import "@segmentstream/app/ui/styles.css";
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Publish page descriptions for ontology agents
|
|
226
|
+
|
|
227
|
+
Page discovery is optional. Wrap the app's Next.js configuration:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { withPageCatalog } from "@segmentstream/app/navigation/next";
|
|
231
|
+
|
|
232
|
+
export default withPageCatalog({});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Keep existing Next plugins and configuration inside the wrapper. Object and async
|
|
236
|
+
function configurations are supported, and an existing
|
|
237
|
+
`compiler.runAfterProductionCompile` hook still runs. The route adapter supports
|
|
238
|
+
stable Next.js 16.x App Router builds.
|
|
239
|
+
|
|
240
|
+
Add `page.navigation.json` beside an ordinary page, using canonical ontology names:
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"title": "Invoices",
|
|
245
|
+
"entities": ["invoice"],
|
|
246
|
+
"description": "Search invoices and open one from the results."
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The SDK derives the route from the page file, including route groups, dynamic and
|
|
251
|
+
catch-all segments, `pageExtensions`, `basePath`, and `trailingSlash`. Optional
|
|
252
|
+
`parameters` and `query` maps describe existing UI behavior. Parameter hints must
|
|
253
|
+
name actual path parameters. Pages may name multiple entities or no entities.
|
|
254
|
+
Page components, data loading, and frontend links do not change.
|
|
255
|
+
|
|
256
|
+
`segmentstream deploy` prepares the catalog from the immutable app source and its
|
|
257
|
+
evaluated ontology before the app and ontology builds. It verifies the final Next
|
|
258
|
+
route inventory after the app's configured build command. The ontology service
|
|
259
|
+
publishes the resulting metadata through its read-only `navigation` MCP tool.
|
|
260
|
+
Sidecars without `withPageCatalog` fail preparation. Missing, invalid, shadowed,
|
|
261
|
+
or unrepresentable annotated routes fail rather than publishing guessed links.
|
|
262
|
+
|
|
263
|
+
For a standalone build, supply the evaluated ontology JSON (`hash` and an
|
|
264
|
+
`entities` array with canonical `name` fields) and immutable source identity:
|
|
265
|
+
|
|
266
|
+
```sh
|
|
267
|
+
npx --no-install segmentstream-page-catalog generate --project-dir . --ontology ../evaluated-ontology.json --source-hash "$SOURCE_HASH"
|
|
268
|
+
npm run build
|
|
269
|
+
npx --no-install segmentstream-page-catalog verify --project-dir .
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The Node-only `@segmentstream/app/navigation/build` entry point exports
|
|
273
|
+
`generatePageCatalog({ files, routing?, ontology, sourceHash })` for pure in-memory
|
|
274
|
+
generation, `preparePageCatalog({ projectDir, ontology, sourceHash })` for resolved
|
|
275
|
+
Next configuration and artifact preparation, and
|
|
276
|
+
`verifyPageCatalogBuild({ projectDir })` for final build checks. File-map keys are
|
|
277
|
+
app-root relative, such as `src/app/invoices/page.tsx`; values are source strings.
|
|
278
|
+
Preparation also reports annotated and unannotated page coverage.
|
|
279
|
+
|
|
280
|
+
Ignore `.segmentstream/navigation/` in Git. Its atomically replaced
|
|
281
|
+
`page-catalog.json` is the deployment artifact; `build-context.json` and
|
|
282
|
+
`compiled.json` are disposable build inputs/evidence and are not runtime settings.
|
|
283
|
+
Failed generation or verification removes previous output. Custom build commands
|
|
284
|
+
must retain the compiler hook and run final verification; a prepared catalog
|
|
285
|
+
alone does not prove the app was built.
|
|
286
|
+
|
|
287
|
+
An intercepted annotation has no independent URL and must describe its ordinary
|
|
288
|
+
destination or containing page. A parallel annotation requires an ordinary
|
|
289
|
+
direct-load page at the same URL. Unannotated variants do not prevent adoption.
|
|
290
|
+
Redirects, `beforeFiles` rewrites, and `afterFiles` rewrites preceding a dynamic
|
|
291
|
+
page fail validation when they may intercept an advertised route. Dynamic routes are
|
|
292
|
+
compared by complete path shape, including optional and catch-all segments. Custom
|
|
293
|
+
regular expressions remain conservative when static prefixes cannot prove them
|
|
294
|
+
disjoint. Runtime proxy/redirect behavior remains the workspace's responsibility.
|
|
295
|
+
|
|
296
|
+
## Read the signed-in user
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
import { segmentstream } from "@/segmentstream";
|
|
300
|
+
|
|
301
|
+
export default async function Page() {
|
|
302
|
+
const user = await segmentstream.requireUser();
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<p>
|
|
306
|
+
{user.name ?? user.email}: {user.roles.join(", ")}
|
|
307
|
+
</p>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
- `requireUser()` returns `{ id, email?, name?, roles, permissions, scopes?, kind?,
|
|
313
|
+
active?, authUserId?, organizationId, workspaceId }`. `id` is the person's ontology `user`
|
|
314
|
+
record. Roles and permissions come from the ontology's `GET /api/ontology/me`,
|
|
315
|
+
cached per session for a minute; `requireUser({ fresh: true })` asks again and
|
|
316
|
+
caches the new answer. Signed out, it redirects to `/sign-in`. For a
|
|
317
|
+
person the app has not invited (`403 not_invited`), it redirects to
|
|
318
|
+
`/sign-in?error=not_invited`, where `<SignIn />` asks them to request an invitation
|
|
319
|
+
or use another email.
|
|
320
|
+
- `getUser()` returns the same, or `null` when signed out, and takes `{ fresh }`; it throws
|
|
321
|
+
`SegmentStreamApiError` with `code: "not_invited"` for people without an invitation.
|
|
322
|
+
- `can("lead:changeStatus")` is `true` when the user's permissions cover the
|
|
323
|
+
permission. Roles may hold wildcards: `lead:*`, `*:read`, `*:*`. `isAllowed` and
|
|
324
|
+
`grants` apply the same rule to any list of permissions.
|
|
325
|
+
- `scopes`, present only when a held permission covers some records only (a
|
|
326
|
+
[scoped permission](../../docs/ontology.md#scoped-permissions),
|
|
327
|
+
ontology 0.40.0 and later), lists them as `{ permission, where }`. `permission` is
|
|
328
|
+
spelled as in `permissions`, which still holds it, so `can()` is `true`; `where` is
|
|
329
|
+
the condition in the wire form of a query, the user as `{ "$me": [] }` and a path
|
|
330
|
+
from them as `{ "$me": ["staff_member", "division"] }`. It is informational, for
|
|
331
|
+
what the app shows: the ontology service enforces the scope on every read and write.
|
|
332
|
+
- The cached roles decide what the app shows. Every `segmentstream.ontology` request is
|
|
333
|
+
checked by the ontology service with the member's current roles, so it never needs
|
|
334
|
+
`fresh`. Pass `can(permission, { fresh: true })` where code outside the ontology, such
|
|
335
|
+
as a warehouse query or a job start, is gated on a permission that a role change must
|
|
336
|
+
take away at once.
|
|
337
|
+
- `signOut()` ends the session, clears its cookies and redirects to `/sign-in` (or
|
|
338
|
+
`{ redirectTo }`). Call it from a Server Function or Route Handler.
|
|
339
|
+
|
|
340
|
+
Use them in Server Components, Route Handlers and Server Functions. Always check the
|
|
341
|
+
user in Server Functions and Route Handlers too; do not rely on the proxy alone.
|
|
342
|
+
|
|
343
|
+
## Client components
|
|
344
|
+
|
|
345
|
+
`@segmentstream/app/ui` holds React client components with plain, neutral CSS
|
|
346
|
+
(no Tailwind). Every class starts with `ss-`; the `--ss-*` custom properties on
|
|
347
|
+
`:root` set colours, radius and font.
|
|
348
|
+
|
|
349
|
+
| Export | Does |
|
|
350
|
+
| -------------------------------------- | ------------------------------------------------------------------------------- |
|
|
351
|
+
| `<SignIn />` | Email, then a six-digit code or a sign-in link; resend after 30 seconds, errors |
|
|
352
|
+
| `useSignIn()` | The same sign-in without markup, for the app's own form |
|
|
353
|
+
| `<SignedIn>`, `<SignedOut>` | Render children by session state |
|
|
354
|
+
| `<Can permission="lead:changeStatus">` | Renders children when the user holds the permission; `fallback` otherwise |
|
|
355
|
+
| `<UserMenu />` | Name, email, roles and sign-out |
|
|
356
|
+
| `<AgentConnect />` | The agent connect page: the pairing, an Approve button and the result |
|
|
357
|
+
| `useUser()` | `{ status, user, error }` from `/api/ontology/me`, loaded once per session |
|
|
358
|
+
| `usePermissions()` | `{ isLoading, roles, permissions, can(permission) }` |
|
|
359
|
+
| `useSession()`, `signOut()` | The selected browser session; sign-out from the browser |
|
|
360
|
+
|
|
361
|
+
For an OIDC or channel session, `signOut()` revokes the selected grant before clearing browser
|
|
362
|
+
state and navigating. It rejects on revocation failure, keeping the session available for retry.
|
|
363
|
+
|
|
364
|
+
`<Can>` only hides controls: the ontology service checks every read and action. A
|
|
365
|
+
Server Component may wrap server-rendered children in `<Can>`:
|
|
366
|
+
|
|
367
|
+
```tsx
|
|
368
|
+
<Can permission="lead:changeStatus" fallback={lead.status}>
|
|
369
|
+
<ChangeStatusForm lead={lead.id} />
|
|
370
|
+
</Can>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Your own sign-in form
|
|
374
|
+
|
|
375
|
+
This email controller applies only when `email` is enabled. For declared
|
|
376
|
+
SegmentStream login use `useBrowserSignIn()` and supply workspace-owned text.
|
|
377
|
+
|
|
378
|
+
`useSignIn({ returnTo, error })` holds everything `<SignIn />` does, without markup: the
|
|
379
|
+
sign-in check, the steps, the 30-second resend hold, the six-digit check, error sentences
|
|
380
|
+
and their codes, the return to `returnTo` after sign-in and the `not_invited` and
|
|
381
|
+
`sign_in_link_invalid` states. Both options default to the page's query parameters.
|
|
382
|
+
|
|
383
|
+
`submitEmail()` first asks the ontology's `POST /api/ontology/sign-in/check` whether the
|
|
384
|
+
address may sign in. A member who has signed in with Neon Auth before gets a six-digit
|
|
385
|
+
code (`step: "code"`). A first sign-in gets a sign-in link instead (`step: "link"`),
|
|
386
|
+
because Neon Auth sends codes only to existing accounts and the link creates the
|
|
387
|
+
account. An address the ontology refuses gets `errorCode: "not_invited"`, and nothing
|
|
388
|
+
is sent. Without an ontology that answers the check (none, or one deployed with
|
|
389
|
+
`@segmentstream/ontology` before 0.4), every address gets a code. The workspace starter renders it with
|
|
390
|
+
Tailwind CSS and shadcn/ui in `src/components/sign-in-form.tsx`; `<SignIn />` stays for
|
|
391
|
+
apps without Tailwind.
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
"use client";
|
|
395
|
+
|
|
396
|
+
import { useSignIn } from "@segmentstream/app/ui";
|
|
397
|
+
|
|
398
|
+
export function SignInForm() {
|
|
399
|
+
const signIn = useSignIn();
|
|
400
|
+
|
|
401
|
+
if (signIn.step === "email") {
|
|
402
|
+
return (
|
|
403
|
+
<form
|
|
404
|
+
noValidate
|
|
405
|
+
onSubmit={(event) => {
|
|
406
|
+
event.preventDefault();
|
|
407
|
+
void signIn.submitEmail();
|
|
408
|
+
}}
|
|
409
|
+
>
|
|
410
|
+
<input
|
|
411
|
+
type="email"
|
|
412
|
+
value={signIn.email}
|
|
413
|
+
onChange={(event) => signIn.setEmail(event.target.value)}
|
|
414
|
+
/>
|
|
415
|
+
{signIn.error !== null && <p role="alert">{signIn.error}</p>}
|
|
416
|
+
<button disabled={signIn.pending !== null}>Continue</button>
|
|
417
|
+
</form>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (signIn.step === "link") {
|
|
422
|
+
return (
|
|
423
|
+
<p>
|
|
424
|
+
We sent a sign-in link to {signIn.email}. Open it to sign in.
|
|
425
|
+
<button type="button" onClick={() => void signIn.resend()} disabled={signIn.resendIn > 0}>
|
|
426
|
+
Resend link
|
|
427
|
+
</button>
|
|
428
|
+
</p>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<form
|
|
434
|
+
noValidate
|
|
435
|
+
onSubmit={(event) => {
|
|
436
|
+
event.preventDefault();
|
|
437
|
+
void signIn.submitCode();
|
|
438
|
+
}}
|
|
439
|
+
>
|
|
440
|
+
<input
|
|
441
|
+
inputMode="numeric"
|
|
442
|
+
autoComplete="one-time-code"
|
|
443
|
+
value={signIn.code}
|
|
444
|
+
onChange={(event) => signIn.setCode(event.target.value)}
|
|
445
|
+
/>
|
|
446
|
+
{signIn.error !== null && <p role="alert">{signIn.error}</p>}
|
|
447
|
+
<button disabled={signIn.pending !== null}>Sign in</button>
|
|
448
|
+
<button type="button" onClick={() => void signIn.resend()} disabled={signIn.resendIn > 0}>
|
|
449
|
+
Resend code
|
|
450
|
+
</button>
|
|
451
|
+
</form>
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
| Field | Means |
|
|
457
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
458
|
+
| `step` | `email`, then `code` once a code is sent, or `link` once a sign-in link is sent |
|
|
459
|
+
| `email`, `setEmail`, `code`, `setCode` | The field values; `submitEmail()` trims the address |
|
|
460
|
+
| `pending` | `send`, `verify`, `resend` or `sign-out` while a call runs; disable the form |
|
|
461
|
+
| `error`, `notice` | A sentence for the person, and a confirmation such as a resent code |
|
|
462
|
+
| `errorCode`, `noticeCode` | Why the call failed (`not_invited`, `code_invalid`, …) and what was resent, to reword |
|
|
463
|
+
| `resendIn` | Seconds until `resend()` may be offered; 0 when it may |
|
|
464
|
+
| `submitEmail()`, `submitCode(value?)` | Check, then send a code or link; verify `value` or `code`. A wrong code clears `code` |
|
|
465
|
+
| `resend()`, `changeEmail()` | Another code, or link, to the same address; back to the email step |
|
|
466
|
+
| `signedIn`, `destination` | A session already exists: offer a link to `destination` |
|
|
467
|
+
| `notInvited`, `sessionEmail` | The person is signed in as `sessionEmail` without an invitation |
|
|
468
|
+
| `switchAccount()` | Signs out and returns to the email step |
|
|
469
|
+
| `linkInvalid` | An emailed sign-in link was expired or used: say so on the email step |
|
|
470
|
+
|
|
471
|
+
The calls take values, not form events: call `submitEmail()` and `submitCode()` from the
|
|
472
|
+
form's submit handler after `event.preventDefault()`. A second `submitCode()` while one
|
|
473
|
+
runs is ignored, so the last digit and the Enter key may both submit.
|
|
474
|
+
|
|
475
|
+
## Telegram Mini App sign-in
|
|
476
|
+
|
|
477
|
+
The Mini App is the same app on any page. Enable it explicitly in both SDKs:
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
// App SDK configuration
|
|
481
|
+
createSegmentStreamApp({ ...link, channels: { telegram: true } });
|
|
482
|
+
|
|
483
|
+
// Ontology service configuration (the connection used by chat.telegram)
|
|
484
|
+
createOntologyService(ontology, {
|
|
485
|
+
...config,
|
|
486
|
+
channels: { telegram: { connection: "chat_bot", miniApp: {} } },
|
|
487
|
+
});
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Without these opt-ins, exchange and Telegram framing stay disabled. Providers default to invited:
|
|
491
|
+
paired members can sign in. Workspace apps may explicitly open `signIn.channels.telegram` for new
|
|
492
|
+
accounts and choose their restricted member grants, `onNewUser` action, notice/reply content and
|
|
493
|
+
lower limits. The exchange uses A1's shared admission, limits and onboarding; browser values cannot
|
|
494
|
+
change them. Members without email are managed by member ID.
|
|
495
|
+
|
|
496
|
+
Mount `<TelegramSignIn />` in a client layout on any page, or call `useTelegramSignIn()` and render
|
|
497
|
+
its `{status, error}` codes with app-owned copy. `<SignIn />` automatically exchanges when the app
|
|
498
|
+
opts in and Telegram launch data is present; `renderTelegram` optionally renders its status. No
|
|
499
|
+
dedicated Mini App route is provided. In a top-level WebView, an unauthenticated deep link redirects
|
|
500
|
+
to `/sign-in?returnTo=…`; HTTP redirects inherit Telegram's URL fragment, and the SDK reads
|
|
501
|
+
`tgWebAppData` there. Raw assertions never enter redirect queries, logs or persistent storage.
|
|
502
|
+
For Telegram Web, an unauthenticated iframe navigation instead receives an inert exchange shell
|
|
503
|
+
at the original protected URL. It renders no workspace content and makes one exchange, then checks
|
|
504
|
+
that the browser retained the session cookie before reloading the deep link. This requires the app's
|
|
505
|
+
explicit Telegram opt-in and is limited to iframe GET navigations. Decision pages remain unframeable.
|
|
506
|
+
Missing launch data stays idle; a refusal or failed discovery stops without reloading or falling back
|
|
507
|
+
to another provider. The shell has no UI copy; its `data-channel-sign-in` and
|
|
508
|
+
`data-channel-sign-in-error` attributes expose status and machine codes for inspection.
|
|
509
|
+
If the same launch returns to the shell after its one allowed reload, it stops with `launch_loop`;
|
|
510
|
+
reopen the Mini App with fresh launch data to retry.
|
|
511
|
+
Safari and other browsers without usable CHIPS cannot persist an iframe session; the regular
|
|
512
|
+
sign-in redirect remains DENY inside an iframe, so use a top-level Mini App launch.
|
|
513
|
+
Browsers without Fetch Metadata headers fall back to that DENY sign-in redirect.
|
|
514
|
+
|
|
515
|
+
A failed launch otherwise stays on the blank shell. To explain it in the app's own words, name a
|
|
516
|
+
public page with `channelLaunchFailurePath`:
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
export const proxy = segmentstream.proxy({
|
|
520
|
+
publicPaths: ["/telegram-unavailable"],
|
|
521
|
+
channelLaunchFailurePath: "/telegram-unavailable",
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
After any failure except missing launch data, the shell replaces the frame with that page and
|
|
526
|
+
passes `error`, the machine code (`session_unavailable` when the browser did not keep the session
|
|
527
|
+
cookie, `launch_loop`, `not_paired`, `access_refused` and the other exchange codes), and
|
|
528
|
+
`returnTo`, the launched path and query. Telegram's launch data stays in the fragment and never
|
|
529
|
+
reaches the page. Telegram Web frames the page, and anyone can open it with any query: show only
|
|
530
|
+
copy chosen for known codes, and accept `returnTo` only as a local path, for example to open it in
|
|
531
|
+
a new browser tab, where regular sign-in works. The path must be a plain page covered by
|
|
532
|
+
`publicPaths`, never a decision page or an `/api`, `/mcp`, `/.well-known` or `/_next` route; the
|
|
533
|
+
proxy refuses other values.
|
|
534
|
+
Apps using Telegram's Web App script can also supply fresh data from `Telegram.WebApp.initData`:
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
"use client";
|
|
538
|
+
import { TelegramSignIn } from "@segmentstream/app/ui";
|
|
539
|
+
|
|
540
|
+
export function ChannelSession() {
|
|
541
|
+
return <TelegramSignIn />;
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
The hook posts each fresh assertion once. A single timer renews at the discovered session's
|
|
546
|
+
`expires_at`, without polling. Supply `initData={() => freshRawInitData}` for a custom integration;
|
|
547
|
+
if Telegram still supplies an expired assertion, reopen the Mini App. The app owns refusal text.
|
|
548
|
+
|
|
549
|
+
A valid channel session takes precedence over regular OIDC and Neon sessions on server, proxy and
|
|
550
|
+
browser. Stale channel cookies fall through to regular sign-in. `requireUser`, ontology transport,
|
|
551
|
+
`useSession`, `useUser`, permissions and logout use that selection. JavaScript receives session
|
|
552
|
+
metadata, never the opaque token. `getAccessToken()` remains the legacy SegmentStream credential
|
|
553
|
+
API, and legacy SegmentStream-mode `can()` keeps its previous behavior. Routes live outside
|
|
554
|
+
`/api/auth/*`.
|
|
555
|
+
|
|
556
|
+
Session discovery, configuration and proxy verification/refresh outages remain `unavailable`; they never
|
|
557
|
+
select another provider's user or start an assertion exchange. Retry session discovery or reload
|
|
558
|
+
after recovery. Malformed channel/OIDC identity responses also stop authentication; OIDC discovery
|
|
559
|
+
retains upstream failures instead of reporting a signed-out session. Only confirmed absence or
|
|
560
|
+
refusal is treated as signed out.
|
|
561
|
+
|
|
562
|
+
Browser discovery probes Neon only when the app configures it. Telegram sign-in also works in
|
|
563
|
+
legacy SegmentStream apps without Neon or OIDC. A separate `SEGMENTSTREAM_ONTOLOGY_URL` uses its
|
|
564
|
+
HTTP(S) origin consistently for channel discovery and protected-page validation.
|
|
565
|
+
Public proxy paths and OAuth metadata discovery bypass cookie verification in every authentication
|
|
566
|
+
mode. Unsupported metadata returns 404 without starting sign-in.
|
|
567
|
+
|
|
568
|
+
Sessions last one hour absolutely, without refresh tokens. HTTPS cookies are HttpOnly, Secure,
|
|
569
|
+
SameSite=None and Partitioned. Logout revokes the grant and clears the cookie. Renewal, unpairing
|
|
570
|
+
and pairing displacement revoke only that identity's grants; blocking/deactivation revoke all of
|
|
571
|
+
that member's sessions. Writes, exchange and logout require the app's Origin. Framing by Telegram
|
|
572
|
+
Web is enabled only with app opt-in; sign-in, consent and pairing pages always remain DENY.
|
|
573
|
+
|
|
574
|
+
Before release, manually check Telegram iOS, Android, Desktop and Web: deep-link launch/fragment retention,
|
|
575
|
+
exchange, reload, expiry/fresh data, partitioned-cookie persistence, logout/reopen, mixed sessions
|
|
576
|
+
and revoked/blocked/deactivated members. These checks need a deployed HTTPS app and are separate
|
|
577
|
+
from local fixtures. CI runs the HTTPS iframe suite when Chrome is installed; opt in locally with
|
|
578
|
+
`CHROME_BIN=/usr/bin/google-chrome pnpm --filter @segmentstream/app test --maxWorkers=1 tests/telegram-launch.browser.test.ts`.
|
|
579
|
+
It uses Chrome and OpenSSL installed on the host and a temporary self-signed certificate.
|
|
580
|
+
|
|
581
|
+
## The ontology
|
|
582
|
+
|
|
583
|
+
`segmentstream.ontology.<entity>` calls the app's ontology service at `/api/ontology`
|
|
584
|
+
with the signed-in user's selected workspace session, so the service applies their permissions.
|
|
585
|
+
Records, action inputs and action results are typed from the ontology:
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
const { items, nextCursor } = await segmentstream.ontology.lead.find({
|
|
589
|
+
where: { status: { in: ["new", "contacted"] } },
|
|
590
|
+
orderBy: [{ field: "createdAt", direction: "desc" }],
|
|
591
|
+
include: ["owner"],
|
|
592
|
+
limit: 20,
|
|
593
|
+
});
|
|
594
|
+
const lead = await segmentstream.ontology.lead.get(id);
|
|
595
|
+
const history = await segmentstream.ontology.lead.history(id, { limit: 50 });
|
|
596
|
+
const page = await segmentstream.ontology.lead.query({ where: { email: { startsWith: "ada" } } });
|
|
597
|
+
|
|
598
|
+
const input = { lead: id, status: "contacted" } as const;
|
|
599
|
+
const check = await segmentstream.ontology.lead.actions.changeStatus(input, {
|
|
600
|
+
validateOnly: true,
|
|
601
|
+
});
|
|
602
|
+
const { result, validation, edits } = await segmentstream.ontology.lead.actions.changeStatus(input);
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
- `find` sends the query as URL parameters, `query` as a JSON body; both return
|
|
606
|
+
`{ items, nextCursor }`. Pass `nextCursor` as `after` for the next page.
|
|
607
|
+
- `include` adds related records under their relationship names: list the names,
|
|
608
|
+
`["owner", "tags"]`, or pass `{ owner: true, tags: true }`.
|
|
609
|
+
- `select: ["email", "status"]` returns only those base fields, plus `id` and
|
|
610
|
+
any requested `include` relationships. An empty list returns ids only. Selected
|
|
611
|
+
pages carry `projected: true`; the typed client marks unselected fields optional.
|
|
612
|
+
Filtering and cursor ordering may use unselected fields. Omit `select` to receive
|
|
613
|
+
complete records. `get` and action reads always return complete records.
|
|
614
|
+
- `actions.<name>(input, { validateOnly })` posts `{ input, options }` and returns
|
|
615
|
+
`{ result, validation, edits }`. Generated actions are there too
|
|
616
|
+
(`actions.create`, `update`, `delete`), and `action(name, input)` runs any action by
|
|
617
|
+
name, such as `upsert_by_email`.
|
|
618
|
+
- `segmentstream.ontology.user` reads members and services. Each record has `roles`, the
|
|
619
|
+
role names assigned to it without `member`, spelled as `requireUser().roles` spells
|
|
620
|
+
them; filter on them with `where: { roles: { has: "sales" } }` or
|
|
621
|
+
`{ roles: { hasAny: ["sales", "support"] } }`. Its generated actions change them:
|
|
622
|
+
`actions.invite({ email, name?, roles? })`, `grantRoles({ id, roles })`,
|
|
623
|
+
`revokeRoles({ id, roles })` or `revokeRoles({ id, all: true })`, `deactivate({ id })`
|
|
624
|
+
and `reactivate({ id })`. Each needs its explicit `user:<verb>` permission, which
|
|
625
|
+
`*:*` does not cover, writes history and returns the updated record. List `User` (or
|
|
626
|
+
`User.actions(…)`) in the ontology's `entities` to type `segmentstream.ontology.user`.
|
|
627
|
+
- Failures throw `SegmentStreamApiError` with the service's `code` (`forbidden`,
|
|
628
|
+
`not_found`, `invalid_input`, `action_rejected`, `conflict`, `not_invited`), `status`,
|
|
629
|
+
`detail`, `requestId` and, for invalid input, `issues`.
|
|
630
|
+
- The types are read from the ontology SDK's `Ontology`, entity and action types
|
|
631
|
+
structurally, so the app needs neither the ontology SDK nor Effect at runtime.
|
|
632
|
+
Without a type argument, any entity name is accepted and records are untyped.
|
|
633
|
+
|
|
634
|
+
### Upload files
|
|
635
|
+
|
|
636
|
+
In a Neon Auth app, import the browser client from `@segmentstream/app/storage`
|
|
637
|
+
and send its metadata requests to the app's own ontology path:
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
import { createBlobStorageClient } from "@segmentstream/app/storage";
|
|
641
|
+
|
|
642
|
+
const files = createBlobStorageClient({
|
|
643
|
+
fetch: (path, init) => fetch(`/api/ontology${path}`, { ...init, credentials: "same-origin" }),
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// In the file picker: keep this UUID for retries of this file's upload.
|
|
647
|
+
const uploadKey = crypto.randomUUID();
|
|
648
|
+
const uploaded = await files.uploadForAction(
|
|
649
|
+
{ entity: "document", property: "content", action: "create" },
|
|
650
|
+
selectedFile,
|
|
651
|
+
{ idempotencyKey: uploadKey },
|
|
652
|
+
);
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
The browser attaches the member's HttpOnly Neon session cookie automatically. The
|
|
656
|
+
ontology checks the target action's permission before issuing a transfer grant;
|
|
657
|
+
missing permission answers `403`. File bytes go directly to storage without member
|
|
658
|
+
credentials. Import the `/storage` entry point in browser code; the package root
|
|
659
|
+
loads the server-side app integration. No token handling or `segmentstream.fetch`
|
|
660
|
+
call is needed.
|
|
661
|
+
|
|
662
|
+
Send `uploaded.ticket` and `uploaded.options` as JSON to a workspace Server Function
|
|
663
|
+
or Route Handler. As the same signed-in member, server code calls:
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
await segmentstream.ontology.document.actions.create(
|
|
667
|
+
{ title: "Contract", content: { uploadId: uploaded.ticket.uploadId } },
|
|
668
|
+
uploaded.options,
|
|
669
|
+
);
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Upload completion alone does not change a record. Keep the upload result and retry
|
|
673
|
+
the action with the same input and options if its response is lost; do not restart
|
|
674
|
+
an upload after its ticket has been consumed. The helper supplies `createIntent`
|
|
675
|
+
for a new record; use `recordId` in the target when changing an existing record.
|
|
676
|
+
The deployed blob property, target action and workspace quota must permit the file.
|
|
677
|
+
The returned `uploaded.upload` contains the session ID, state and expiry. An optional
|
|
678
|
+
`onSession(session)` callback records that ID as soon as the server creates it, even
|
|
679
|
+
if a later transfer fails. `onProgress` reports byte transfer, not record creation.
|
|
680
|
+
|
|
681
|
+
Recover your current open sessions with `await files.list()`: it returns `pending`,
|
|
682
|
+
`max` and `uploads`, including filename, target action, expiry and `awaitingAction`.
|
|
683
|
+
A `ready` session with `awaitingAction: true` has uploaded bytes and still needs its
|
|
684
|
+
saving action. Use `files.status(id)` to inspect its ticket. When you abandon that
|
|
685
|
+
action, including after a permanent rejection, call `await files.cancel(id)` to
|
|
686
|
+
release the slot. Keep the session when you intend to retry; do not cancel merely
|
|
687
|
+
because an action returned an error or its response was lost. Saved records cannot
|
|
688
|
+
be canceled through this API. Discovery requires an upgraded, redeployed ontology;
|
|
689
|
+
the existing status and cancel endpoints also work with earlier SDK deployments.
|
|
690
|
+
|
|
691
|
+
`SegmentStreamApiError.limit` preserves structured platform refusals: `storage_quota`
|
|
692
|
+
has `usedBytes`, `requestedBytes` and `quotaBytes`; `pending_uploads` has `pending`,
|
|
693
|
+
`max` and, when supplied, the open upload IDs, filenames and `expiresAt` values;
|
|
694
|
+
`member_attachments` has `live` and `max`. Workspace code supplies the user-facing
|
|
695
|
+
explanation from these fields.
|
|
696
|
+
See the complete [browser upload and Server Function example](../../docs/app-data.md#upload-an-ontology-file)
|
|
697
|
+
for authentication, retries, cancellation, and local development.
|
|
698
|
+
|
|
699
|
+
### Download files
|
|
700
|
+
|
|
701
|
+
Server Components and Route Handlers request blob download grants through the same
|
|
702
|
+
member-authenticated client, including under Neon Auth:
|
|
703
|
+
|
|
704
|
+
```ts
|
|
705
|
+
const grant = await segmentstream.ontology.document.files.download(id, "content", {
|
|
706
|
+
inline: false,
|
|
707
|
+
});
|
|
708
|
+
// grant: { url, method: "GET", headers, expiresAt }
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
The ontology checks current `document:read` permission and record scopes. The
|
|
712
|
+
property name is typed to blob properties; options also accept `revisionId` and
|
|
713
|
+
`signal`. Missing permission throws `SegmentStreamApiError` with status `403`;
|
|
714
|
+
an inaccessible record or revision answers `404`. The helper never reads file bytes.
|
|
715
|
+
|
|
716
|
+
Use a Route Handler to obtain a fresh grant and redirect the browser to its signed
|
|
717
|
+
GET URL with `Cache-Control: private, no-store`. The browser downloads directly
|
|
718
|
+
from storage, including files over 2 MiB. Keep the app route as the durable link,
|
|
719
|
+
since grants expire. See the complete
|
|
720
|
+
[Route Handler example](../../docs/app-data.md#download-an-ontology-file).
|
|
721
|
+
|
|
722
|
+
Normal app downloads need no custom transport or token handling, and do not use
|
|
723
|
+
`segmentstream.fetch` or the workspace machine credential. Browser uploads use
|
|
724
|
+
the separate [storage client](#upload-files).
|
|
725
|
+
|
|
726
|
+
## Agent connect page
|
|
727
|
+
|
|
728
|
+
The workspace agent answers a chat account it does not know, such as a Telegram user,
|
|
729
|
+
with a link to `/agent/connect?code=<code>`. A signed-in member approves there, and the
|
|
730
|
+
ontology links the account to them: the agent then acts as that member, with their
|
|
731
|
+
current roles, for that account's messages. The proxy signs the page in like any other
|
|
732
|
+
and forbids other sites to frame it.
|
|
733
|
+
|
|
734
|
+
```tsx
|
|
735
|
+
// src/app/agent/connect/page.tsx, without Tailwind
|
|
736
|
+
import { SegmentStreamApiError, type AgentPairing } from "@segmentstream/app";
|
|
737
|
+
import { AgentConnect } from "@segmentstream/app/ui";
|
|
738
|
+
import { segmentstream } from "@/segmentstream";
|
|
739
|
+
|
|
740
|
+
type SearchParams = Promise<Record<string, string | string[] | undefined>>;
|
|
741
|
+
|
|
742
|
+
export default async function ConnectPage({ searchParams }: { searchParams: SearchParams }) {
|
|
743
|
+
const user = await segmentstream.requireUser();
|
|
744
|
+
const { code, connected } = await searchParams;
|
|
745
|
+
let pairing: AgentPairing | null = null;
|
|
746
|
+
|
|
747
|
+
if (typeof code === "string" && typeof connected !== "string") {
|
|
748
|
+
try {
|
|
749
|
+
pairing = await segmentstream.agent.pairing(code);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
// A used or expired code shows "This link has expired"; anything else is an error.
|
|
752
|
+
if (
|
|
753
|
+
!(error instanceof SegmentStreamApiError) ||
|
|
754
|
+
!["not_found", "expired"].includes(error.code)
|
|
755
|
+
) {
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return (
|
|
762
|
+
<AgentConnect
|
|
763
|
+
code={typeof code === "string" ? code : undefined}
|
|
764
|
+
pairing={pairing}
|
|
765
|
+
connected={typeof connected === "string" ? connected : undefined}
|
|
766
|
+
member={user.email ?? undefined}
|
|
767
|
+
/>
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
```ts
|
|
773
|
+
// src/app/api/agent/connect/route.ts
|
|
774
|
+
import { segmentstream } from "@/segmentstream";
|
|
775
|
+
|
|
776
|
+
export const { POST } = segmentstream.agent.connectHandler();
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
`segmentstream.agent` holds:
|
|
780
|
+
|
|
781
|
+
| Call | Does |
|
|
782
|
+
| ---------------------- | -------------------------------------------------------------------------------------- |
|
|
783
|
+
| `pairing(code)` | `{ provider, display, expiresAt }`; throws `not_found` or `expired` |
|
|
784
|
+
| `approvePairing(code)` | Links the account to the signed-in member; returns the `AgentIdentity` |
|
|
785
|
+
| `identities()` | The member's own: `{ id, provider, externalId, display, createdAt, revokedAt }` |
|
|
786
|
+
| `revokeIdentity(id)` | Revokes one of them at once |
|
|
787
|
+
| `connectHandler()` | `{ POST }` for the page's form: same-origin only; `303` on success; throws on conflict |
|
|
788
|
+
|
|
789
|
+
The handler sends the browser back to `/agent/connect?connected=<provider>`, or to
|
|
790
|
+
`/agent/connect?expired=1` when the code was used or is older than ten minutes. An account already
|
|
791
|
+
connected to another member throws `SegmentStreamApiError` with status `409` and
|
|
792
|
+
`code === "identity_in_use"` through the app's existing error mechanism. The app owns conflict copy.
|
|
793
|
+
Same-member pairing is idempotent, and replacing your own previous account on a provider is supported. Other
|
|
794
|
+
refusals, such as `forbidden` for a member the ontology does not admit, throw
|
|
795
|
+
`SegmentStreamApiError`. All calls are server-only and run as the signed-in member;
|
|
796
|
+
the ontology decides every one. The workspace starter renders the page with Tailwind CSS
|
|
797
|
+
and shadcn/ui; apps made from an earlier starter add `src/app/agent/connect/page.tsx`
|
|
798
|
+
and `src/app/api/agent/connect/route.ts` from it.
|
|
799
|
+
|
|
800
|
+
## Query the warehouse
|
|
801
|
+
|
|
802
|
+
```tsx
|
|
803
|
+
import { sql } from "@segmentstream/app";
|
|
804
|
+
import { segmentstream } from "@/segmentstream";
|
|
805
|
+
|
|
806
|
+
type Revenue = { order_date: string; total_revenue: number };
|
|
807
|
+
|
|
808
|
+
export default async function Page() {
|
|
809
|
+
await segmentstream.requireUser();
|
|
810
|
+
|
|
811
|
+
const { rows } = await segmentstream.warehouse.query<Revenue>(
|
|
812
|
+
`SELECT order_date, total_revenue
|
|
813
|
+
FROM gold.revenue_summary
|
|
814
|
+
WHERE order_date >= @from
|
|
815
|
+
ORDER BY order_date`,
|
|
816
|
+
{ params: { from: sql.date("2026-09-01") } },
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
return <pre>{JSON.stringify(rows, null, 2)}</pre>;
|
|
820
|
+
}
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
- Warehouse calls authenticate with the workspace app credential,
|
|
824
|
+
`SEGMENTSTREAM_APP_CREDENTIAL`, which SegmentStream sets on deploy and the SDK reads
|
|
825
|
+
on every call. Without it they use the legacy sign-in's session token. The
|
|
826
|
+
credential belongs to the app, so check the user before querying for them.
|
|
827
|
+
- Queries are read-only GoogleSQL over the workspace warehouse. `dataset.table` resolves
|
|
828
|
+
to the workspace's project. Anything other than a single `SELECT` is rejected.
|
|
829
|
+
- Pass values as named `@parameters` in `params`; never build SQL from user input.
|
|
830
|
+
Strings, booleans, `bigint`, safe integers (`INT64`), other numbers (`FLOAT64`), `Date`
|
|
831
|
+
(`TIMESTAMP`) and arrays of these are typed automatically. Use `sql.date()`,
|
|
832
|
+
`sql.datetime()`, `sql.timestamp()`, `sql.numeric()`, `sql.bignumeric()`, `sql.int64()`
|
|
833
|
+
and `sql.array(type, values)` for anything else.
|
|
834
|
+
- `query()` waits for running queries (`timeoutMs`, default 60 s) and follows result
|
|
835
|
+
pages up to `maxRows` (default 10,000); `truncated` reports whether more rows exist.
|
|
836
|
+
`maximumBytesBilled` lowers the per-query byte ceiling; `warehouse.dryRun()` returns the
|
|
837
|
+
estimated bytes and result columns without running the query.
|
|
838
|
+
- Long queries keep running in the warehouse for up to an hour after `query()` stops
|
|
839
|
+
waiting. It then throws `query_timeout` with a `queryId`. Resume the query from a later
|
|
840
|
+
request, for example a Route Handler your page polls, with `warehouse.result(queryId)`.
|
|
841
|
+
Don't hold one request open for minutes.
|
|
842
|
+
- Values arrive as JSON: safe integers as numbers, larger integers and decimals as strings,
|
|
843
|
+
timestamps as ISO strings, records and arrays as JSON strings.
|
|
844
|
+
|
|
845
|
+
One authorization model covers the ontology: every `segmentstream.ontology` call, metric queries
|
|
846
|
+
included, runs as the signed-in user through the ontology service, which checks their
|
|
847
|
+
permissions. `segmentstream.ontology.<entity>.queryMetrics(request)` posts to
|
|
848
|
+
`/api/ontology/<entity>/query_metrics` and needs read permission on the entities it reaches.
|
|
849
|
+
`segmentstream.warehouse` uses the workspace app credential and reaches the whole workspace
|
|
850
|
+
warehouse. The job helpers below use it for explicitly opted-in jobs. `segmentstream.fetch(path)` still requires the legacy
|
|
851
|
+
platform session. Metric queries through the service need the workspace's ontology deployed with
|
|
852
|
+
`@segmentstream/ontology` 0.13.0 or later.
|
|
853
|
+
|
|
854
|
+
The next release names analytics as the ontology does: a request for metrics grouped by
|
|
855
|
+
dimensions is a metric query, `queryMetrics`, which takes `metrics` and `orderBy[].metric`. Its
|
|
856
|
+
types are `MetricQuery`, `EntityMetricQuery`, `MetricQueryResult`, `MetricQueryField`,
|
|
857
|
+
`MetricQueryFilter`, `MetricQueryFilterOperator` and `MetricQueryGranularity`, and the client
|
|
858
|
+
reads an ontology's `metrics`. Earlier releases named these calls and types differently; the
|
|
859
|
+
sections below use the current names. Code that uses the earlier names keeps compiling and
|
|
860
|
+
working as deprecated aliases, which the release notes list, and the client also reads the
|
|
861
|
+
metrics of an ontology built by an earlier ontology SDK. The client posts to
|
|
862
|
+
`/api/ontology/query_metrics` and `/api/ontology/<entity>/query_metrics`. An ontology service
|
|
863
|
+
deployed with an ontology SDK before manifest version 14 serves metric queries only at their
|
|
864
|
+
earlier routes, with the earlier names, and answers 404 at these: the client then posts the
|
|
865
|
+
query there, and once that route answers, sends the service's later queries there directly.
|
|
866
|
+
|
|
867
|
+
Since 0.10.0, dimensions, filters and `orderBy` accept relationship paths, typed from the
|
|
868
|
+
ontology's relationships:
|
|
869
|
+
|
|
870
|
+
```ts
|
|
871
|
+
const { rows, execution } = await segmentstream.ontology.order.queryMetrics({
|
|
872
|
+
dimensions: ["customer.segment"],
|
|
873
|
+
metrics: ["orders"],
|
|
874
|
+
dateRange: { from: "2026-09-01", to: "2026-09-30" },
|
|
875
|
+
});
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
- A path is `<relationship>.<property>`, through one or two to-one relationships. Paths into
|
|
879
|
+
a stored target include `id`; json and blob properties are left out. A thunk target types
|
|
880
|
+
its paths as `` `customer.${string}` ``. Each segment converts to snake_case on the wire,
|
|
881
|
+
and rows come back keyed by the camelCase path. These names are relative to the entity and
|
|
882
|
+
convert as record fields do: `line_2` reads as `line2`.
|
|
883
|
+
- `execution.warnings` reports keys that matched more than one related row; totals are
|
|
884
|
+
unaffected. Warnings are absent when the query produced none. Stored replicas are eventually
|
|
885
|
+
consistent; current responses do not report `execution.sources` or a destination freshness watermark.
|
|
886
|
+
- 0.10.0 requires the platform release that joins stored targets through replication.
|
|
887
|
+
- See [relationship paths](../../docs/ontology-metrics.md#relationship-paths).
|
|
888
|
+
|
|
889
|
+
Since 0.10.0, `segmentstream.ontology.queryMetrics(request)` asks for numbers about the whole
|
|
890
|
+
model: metrics of several entities, warehouse-backed or stored, grouped by properties they all
|
|
891
|
+
reach. Look at or change records through `find` and actions; count, sum or compare through
|
|
892
|
+
`queryMetrics`.
|
|
893
|
+
|
|
894
|
+
```ts
|
|
895
|
+
const { fields, rows, execution } = await segmentstream.ontology.queryMetrics({
|
|
896
|
+
metrics: ["acmeSales.revenue", "acmeStock.onHand"],
|
|
897
|
+
dimensions: ["acmeProduct.assortmentStatus"],
|
|
898
|
+
filters: [
|
|
899
|
+
{
|
|
900
|
+
dimension: "acmeSales.orderDate",
|
|
901
|
+
operator: "between",
|
|
902
|
+
values: ["2026-09-01", "2026-09-30"],
|
|
903
|
+
},
|
|
904
|
+
{ dimension: "acmeSales.currency", operator: "equals", value: "RUB" },
|
|
905
|
+
],
|
|
906
|
+
orderBy: [{ metric: "acmeSales.revenue", direction: "desc" }],
|
|
907
|
+
});
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
- Names are `entity.metric` and `entity.property`, each segment in camelCase, typed from the
|
|
911
|
+
ontology. An `_` before a digit stays, so an entity's declared name converts back:
|
|
912
|
+
`sales_2025.revenue`. This differs from the per-entity call and records, which drop it. Fields
|
|
913
|
+
and rows are keyed by these names.
|
|
914
|
+
- When an entity reaches a property's entity in more than one way, write a route from it:
|
|
915
|
+
`acmeSales.returnedProduct.brand`. Its column is still keyed by the property's own name,
|
|
916
|
+
`acmeProduct.brand`.
|
|
917
|
+
- It posts to the ontology service's `/api/ontology/query_metrics` as the signed-in user, who
|
|
918
|
+
needs read permission on every entity the request reaches. Previews and local development
|
|
919
|
+
answer `aggregate_unavailable`.
|
|
920
|
+
- Date bounds are `between` filters, inclusive, or `inPeriod` filters. Each partitioned entity
|
|
921
|
+
needs one on its own partition property. Currency metrics need their entity filtered to one
|
|
922
|
+
currency, the same for every entity.
|
|
923
|
+
- A metric field's `filters` lists the filters applied to it, and `execution.warnings` names
|
|
924
|
+
each filter that did not reach a metric's entity. `additive: false` marks a metric whose
|
|
925
|
+
rows don't add up to a total.
|
|
926
|
+
- Failures throw `SegmentStreamApiError` with the service's `code`: `invalid_request`,
|
|
927
|
+
`invalid_aggregate_query`, `forbidden`, `rate_limited` (30 metric queries a minute per user),
|
|
928
|
+
`aggregate_unavailable`, `aggregate_upstream_unavailable`, `ontology_version_mismatch` or
|
|
929
|
+
`aggregate_scope_mismatch`. The codes keep their earlier wording. See
|
|
930
|
+
[errors](../../docs/ontology-metrics.md#errors).
|
|
931
|
+
|
|
932
|
+
Since 0.13.0, both metric queries take more conditions, buckets and a timezone:
|
|
933
|
+
|
|
934
|
+
```ts
|
|
935
|
+
const { rows, execution } = await segmentstream.ontology.queryMetrics({
|
|
936
|
+
metrics: ["acmeSales.wonOrders"],
|
|
937
|
+
dimensions: ["acmeSales.orderDate"],
|
|
938
|
+
granularity: "week",
|
|
939
|
+
granularityOf: "acmeSales.orderDate",
|
|
940
|
+
filters: [
|
|
941
|
+
{ dimension: "acmeSales.orderDate", operator: "inPeriod", value: "last 4 weeks" },
|
|
942
|
+
{ dimension: "acmeProduct.brand", operator: "notIn", values: ["Unknown", "Test"] },
|
|
943
|
+
],
|
|
944
|
+
timezone: "Europe/Moscow",
|
|
945
|
+
});
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
- Filter operators are `equals`, `in`, `notEquals`, `notIn`, `isNull`, `isNotNull`, `gt`,
|
|
949
|
+
`gte`, `lt`, `lte`, `contains`, `startsWith` and `endsWith`; the model-level call adds
|
|
950
|
+
`between` and `inPeriod`. `notEquals` and `notIn` also match nulls and rows whose
|
|
951
|
+
relationship found no target. Text operators are case-sensitive and take no wildcards.
|
|
952
|
+
- `inPeriod` takes a period relative to today, typed as `RelativePeriod`: `today`,
|
|
953
|
+
`yesterday`, `this month`, `last quarter`, `this year to date`, `last 7 days`,
|
|
954
|
+
`last 4 weeks`. `execution.periods` gives the dates each one resolved to and the timezone
|
|
955
|
+
that decided today. Only `between` and `inPeriod` bound a partitioned entity.
|
|
956
|
+
- `granularity` is `hour`, `day`, `week`, `month`, `quarter` or `year`; `hour` needs a
|
|
957
|
+
timestamp dimension.
|
|
958
|
+
- `timezone`, an IANA name or `UTC`, replaces the entities' timezones for timestamp buckets,
|
|
959
|
+
date bounds on timestamps and the today of `inPeriod`.
|
|
960
|
+
- A balance metric's field carries `balanceOver`, the date dimension its rows snapshot.
|
|
961
|
+
Group by that dimension or filter it to one day. `balanceOver` and each period's
|
|
962
|
+
`dimension` come back camelCase per segment, like field names.
|
|
963
|
+
- The per-entity call takes the new operators, granularities and `timezone`, but neither
|
|
964
|
+
`between` nor `inPeriod`: `dateRange` stays its partition bound.
|
|
965
|
+
- These requests need app SDK 0.13.0 and the workspace's ontology deployed with
|
|
966
|
+
`@segmentstream/ontology` 0.17.0 or later, whose service accepts them; earlier
|
|
967
|
+
versions refuse them. See [Upgrading to 0.13](#upgrading-to-013).
|
|
968
|
+
|
|
969
|
+
Since 0.14.0, the model-level call also takes model metrics, which the ontology declares in
|
|
970
|
+
`defineOntology({ metrics })` over metrics of several entities, by their bare names beside
|
|
971
|
+
`entity.metric` names:
|
|
972
|
+
|
|
973
|
+
```ts
|
|
974
|
+
const { fields, rows } = await segmentstream.ontology.queryMetrics({
|
|
975
|
+
metrics: ["planAttainment", "acmeSales.ordered"],
|
|
976
|
+
dimensions: ["acmeSales.orderDate"],
|
|
977
|
+
granularity: "month",
|
|
978
|
+
granularityOf: "acmeSales.orderDate",
|
|
979
|
+
filters: [{ dimension: "acmeSales.orderDate", operator: "inPeriod", value: "this year to date" }],
|
|
980
|
+
orderBy: [{ metric: "planAttainment", direction: "desc" }],
|
|
981
|
+
});
|
|
982
|
+
// fields[1]: { name: "planAttainment", label: "Plan attainment", unit: "ratio", additive: false,
|
|
983
|
+
// filters: [...], inputs: ["acmeSales.ordered", "acmePlans.plannedUnits"] }
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
- A model metric is requested by the camelCase name the ontology declares, typed from
|
|
987
|
+
`typeof ontology`. The platform names it in snake_case, `plan_attainment`; its field and row
|
|
988
|
+
key are the camelCase form, `planAttainment`, as for entity metrics.
|
|
989
|
+
- Its field carries `inputs`, the entity metrics it reads, as `entity.metric` in camelCase per
|
|
990
|
+
segment, like other names. They are computed whether or not you request them; request one to
|
|
991
|
+
get its column too.
|
|
992
|
+
- Its `filters` lists the filters that applied to every input. A filter on a property the
|
|
993
|
+
model metric aligns applies to each entity through its own property; a filter that reaches
|
|
994
|
+
some of its entities and not others is refused with `invalid_aggregate_query`.
|
|
995
|
+
- A group one of its entities has no rows for, or a zero denominator, gives `null`.
|
|
996
|
+
- The caller needs read permission on every entity it reads.
|
|
997
|
+
- These requests need the workspace's ontology deployed with `@segmentstream/ontology`
|
|
998
|
+
0.23.0 or later, which declares and serves model metrics; earlier versions refuse them.
|
|
999
|
+
Requests that name none work as before, and their fields carry no `inputs`. See
|
|
1000
|
+
[request model metrics](../../docs/ontology-metrics.md#request-model-metrics).
|
|
1001
|
+
|
|
1002
|
+
## Start a pipeline job
|
|
1003
|
+
|
|
1004
|
+
With app SDK 0.15.0 and pipeline SDK 0.1.0a65 or later, opt in a job and deploy it:
|
|
1005
|
+
|
|
1006
|
+
```python
|
|
1007
|
+
s.job("land_watch", assets=[land_checks], allow_app_runs=True)
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
In server code, check the user's role or permissions before calling on their behalf. The
|
|
1011
|
+
credential is the app's, so ask with `{ fresh: true }` when a revoked role must stop the run at
|
|
1012
|
+
once:
|
|
1013
|
+
|
|
1014
|
+
```ts
|
|
1015
|
+
const user = await segmentstream.requireUser({ fresh: true });
|
|
1016
|
+
if (!user.roles.includes("operator")) {
|
|
1017
|
+
throw new Error("An operator role is required.");
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const run = await segmentstream.jobs.run("land_watch");
|
|
1021
|
+
// From a later authorized request, for example a Route Handler the page polls:
|
|
1022
|
+
const current = await segmentstream.jobs.getRun(run.id);
|
|
1023
|
+
const recent = await segmentstream.jobs.listRuns("land_watch");
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
The helpers use the workspace app credential, read on every call, or fall back to the
|
|
1027
|
+
legacy member session. Credential access defaults to denied unless the current deployed
|
|
1028
|
+
job sets `allow_app_runs=True`. It stays within the workspace and permits submission and
|
|
1029
|
+
status/history reads; run logs, activity and cancellation require platform membership.
|
|
1030
|
+
Removing the flag revokes app access to that job's runs.
|
|
1031
|
+
|
|
1032
|
+
`run()` returns immediately with a run ID and status. It generates a UUID idempotency key
|
|
1033
|
+
unless passed `{ idempotencyKey }`; reuse an explicit key when retrying one button action.
|
|
1034
|
+
Separate calls with new keys can run concurrently. Partitioned jobs accept
|
|
1035
|
+
`{ partitionRange: { startDate, endDate } }`. All helpers accept an optional `signal`.
|
|
1036
|
+
Poll `getRun()` until status is `succeeded`, `failed` or `cancelled`, and tolerate future
|
|
1037
|
+
status values. Protect both submission and status routes with your app's authorization.
|
|
1038
|
+
See [app data access](../../docs/app-data.md#start-a-pipeline-job-from-the-app) for a Route Handler.
|
|
1039
|
+
|
|
1040
|
+
## How requests are handled
|
|
1041
|
+
|
|
1042
|
+
- Page navigations without a session are redirected to `/sign-in?returnTo=<path>` and
|
|
1043
|
+
return to the page after sign-in.
|
|
1044
|
+
- API routes, `fetch` calls, non-GET requests and Next.js data requests receive
|
|
1045
|
+
`401 {"error":"unauthenticated"}` instead of a redirect.
|
|
1046
|
+
- Static assets, `/api/auth/*`, `/sign-in`, `/api/health` and your `publicPaths` never
|
|
1047
|
+
require a session.
|
|
1048
|
+
|
|
1049
|
+
## Environment
|
|
1050
|
+
|
|
1051
|
+
App previews are selected by the platform with `SEGMENTSTREAM_DEPLOYMENT_TARGET=preview`,
|
|
1052
|
+
`SEGMENTSTREAM_PREVIEW_EXPIRES_AT` (an ISO timestamp), and the existing
|
|
1053
|
+
`SEGMENTSTREAM_ONTOLOGY_DEPLOYMENT_ID`. Omitting the target preserves production behavior.
|
|
1054
|
+
Compatibility previews receive an isolated Neon Auth URL and cookie secret.
|
|
1055
|
+
Declared browser methods use exact deployment-bound clients and revision checks;
|
|
1056
|
+
production/sibling credentials and retired revisions are refused. Neither mode
|
|
1057
|
+
falls back to the legacy broker. The preview has its own ontology service and
|
|
1058
|
+
expiring machine credential.
|
|
1059
|
+
|
|
1060
|
+
`segmentstream.deployment` is a serializable `DeploymentObservation`: `target`, optional
|
|
1061
|
+
`deploymentId`, preview `expiresAt`, and `capabilities` (`warehouseRead`, `pipelineRuns`,
|
|
1062
|
+
`connections`, `agents`, `workflows`). A preview enables `warehouseRead` and `workflows` in this profile;
|
|
1063
|
+
its isolated ontology CRUD and file operations remain available under normal user permissions.
|
|
1064
|
+
Workflows support manual starts, action/change triggers and inbound webhooks. Scheduled cron
|
|
1065
|
+
triggers remain disabled. Trusted workspace workflow code can make external network requests
|
|
1066
|
+
and cause real side effects; managed connections, agents and pipeline runs remain unavailable.
|
|
1067
|
+
Pass this observation to your UI to present your own preview indicator and unavailable actions.
|
|
1068
|
+
The SDK supplies codes and values; the workspace supplies human-language text.
|
|
1069
|
+
|
|
1070
|
+
Preview pipeline methods, agent operations and their Route Handlers are refused before external
|
|
1071
|
+
requests. Every server helper, proxy and auth handler checks the deadline, including cached
|
|
1072
|
+
sessions and clients captured before expiry. `DeploymentAdmissionError` exposes stable `code`
|
|
1073
|
+
and `status` values, including `preview_expired` (410), `preview_capability_unavailable` (403)
|
|
1074
|
+
and `preview_configuration_invalid` (503). These SDK observations and guards do not authorize
|
|
1075
|
+
the machine credential: the platform independently enforces its deployment and capability scope.
|
|
1076
|
+
|
|
1077
|
+
| Variable | Purpose |
|
|
1078
|
+
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
1079
|
+
| `NEON_AUTH_BASE_URL` | Email provider URL when email is enabled; environment selection applies only without a browser declaration. |
|
|
1080
|
+
| `SEGMENTSTREAM_APP_AUTH_COOKIE_SECRET` | 32 or more characters; signs the session cache cookie. Required with Neon Auth. |
|
|
1081
|
+
| `SEGMENTSTREAM_APP_CREDENTIAL` | The workspace app credential for warehouse queries and opted-in pipeline jobs. |
|
|
1082
|
+
| `SEGMENTSTREAM_ONTOLOGY_URL` | Optional: the ontology service when it does not run beside the app. |
|
|
1083
|
+
| `SEGMENTSTREAM_URL` | SegmentStream origin. Defaults to `https://cloud.segmentstream.com`. |
|
|
1084
|
+
|
|
1085
|
+
SegmentStream derives browser policy, revision/client bindings and enabled provider
|
|
1086
|
+
configuration on deploy. Explicit methods take precedence over leftover environment
|
|
1087
|
+
variables; missing required bindings fail closed.
|
|
1088
|
+
|
|
1089
|
+
## Local development
|
|
1090
|
+
|
|
1091
|
+
App SDK checks and builds copy the browser-safe channel protocol from
|
|
1092
|
+
`packages/ontology-sdk/src/channel-session.ts` into an ignored source module. The
|
|
1093
|
+
packed app includes that module and does not require the ontology npm package at
|
|
1094
|
+
runtime. Change the canonical ontology source when updating the wire contract.
|
|
1095
|
+
|
|
1096
|
+
For SegmentStream-only local development use `app dev` above and supply an isolated
|
|
1097
|
+
local ontology database. The following applies only to an independently configured
|
|
1098
|
+
email compatibility environment. Use that isolated local Neon Auth URL and a local
|
|
1099
|
+
cookie secret in `app/.env.local`; never copy production credentials. Configure its
|
|
1100
|
+
trusted origins for `http://localhost:3000`:
|
|
1101
|
+
|
|
1102
|
+
```sh
|
|
1103
|
+
NEON_AUTH_BASE_URL=https://ep-….neonauth.….aws.neon.tech/ontology/auth
|
|
1104
|
+
SEGMENTSTREAM_APP_AUTH_COOKIE_SECRET=<openssl rand -base64 32>
|
|
1105
|
+
SEGMENTSTREAM_ONTOLOGY_URL=http://localhost:3100
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
With `SEGMENTSTREAM_ONTOLOGY_URL`, server-side ontology calls go there and the proxy
|
|
1109
|
+
forwards the browser's `/api/ontology/*` requests to it. Point it at a local
|
|
1110
|
+
`npm --prefix ontology run dev` backed by the isolated development database.
|
|
1111
|
+
|
|
1112
|
+
## Security
|
|
1113
|
+
|
|
1114
|
+
Declared SegmentStream browser login keeps an opaque workspace session in an
|
|
1115
|
+
HttpOnly cookie. Ontology admission checks current membership/roles and the parent
|
|
1116
|
+
provider grant; revision and session revocation meet the sixty-second bound.
|
|
1117
|
+
The following JWT/cache details describe email compatibility only.
|
|
1118
|
+
|
|
1119
|
+
- Email sessions are Neon Auth's: `HttpOnly`, `Secure`, `SameSite=Lax` cookies on the app's
|
|
1120
|
+
own domain, set through the app's `/api/auth` route. The session cache cookie is
|
|
1121
|
+
signed with `SEGMENTSTREAM_APP_AUTH_COOKIE_SECRET`.
|
|
1122
|
+
- Signing in proves an email address and grants nothing. The ontology links the
|
|
1123
|
+
person to an invited member, and their roles decide what they may read and do. The
|
|
1124
|
+
ontology service verifies the user's JWT (EdDSA, 15 minutes) and checks activity,
|
|
1125
|
+
roles and permissions on every request. `can()` and `<Can>` read roles cached for up
|
|
1126
|
+
to a minute and decide what the app shows; gate code outside the ontology with
|
|
1127
|
+
`can(permission, { fresh: true })` when a revoked role must stop it at once.
|
|
1128
|
+
- The session JWT and the app credential stay on the server, except that the browser
|
|
1129
|
+
components fetch the JWT from the app's own `/api/auth/token` to read
|
|
1130
|
+
`/api/ontology/me`.
|
|
1131
|
+
- Warehouse queries run with the workspace's own identity; the app never holds
|
|
1132
|
+
warehouse credentials.
|
|
1133
|
+
|
|
1134
|
+
## Legacy compatibility without Neon Auth
|
|
1135
|
+
|
|
1136
|
+
This section applies only without a browser declaration or OIDC binding. Declared
|
|
1137
|
+
SegmentStream-only authentication never invokes this broker fallback.
|
|
1138
|
+
Without `NEON_AUTH_BASE_URL`, the legacy path retains the earlier behavior, so existing apps keep
|
|
1139
|
+
working: members of the workspace's organization sign in with their SegmentStream
|
|
1140
|
+
account through `/api/segmentstream/[...path]/route.ts`
|
|
1141
|
+
(`export const { GET, POST } = segmentstream.handlers();`), the proxy redirects to
|
|
1142
|
+
SegmentStream, and `fetch` acts as the member; warehouse queries prefer the workspace app credential. In that mode
|
|
1143
|
+
`roles` and `permissions` are empty, `can()` is `false`, `authHandlers()` answers
|
|
1144
|
+
`404 neon_auth_not_configured`, and `segmentstream.ontology` sends the member's app
|
|
1145
|
+
token, which the ontology service accepts while workspaces move to Neon Auth. Keep the
|
|
1146
|
+
protocol route for supported existing consumers; this release does not retire the broker.
|
|
1147
|
+
|
|
1148
|
+
## Upgrading from 0.2
|
|
1149
|
+
|
|
1150
|
+
Add the auth route, the sign-in page and the stylesheet import; keep the legacy route.
|
|
1151
|
+
`AppUser` gained `roles` and `permissions`. Under Neon Auth, people sign in again with
|
|
1152
|
+
an email code, and the ontology must have invited them.
|
|
1153
|
+
|
|
1154
|
+
## Upgrading to 0.4
|
|
1155
|
+
|
|
1156
|
+
`useSignIn().step` may now be `link`: render it, or a form written for 0.3 shows the code
|
|
1157
|
+
step after a link was sent. The `<SignIn />` button reads "Continue". Deploy the
|
|
1158
|
+
ontology with `@segmentstream/ontology` 0.4 or later to answer the sign-in check;
|
|
1159
|
+
until then every address gets a code, as in 0.3. `errorCode` and `noticeCode` let an
|
|
1160
|
+
app word messages in its own language without matching the English sentences.
|
|
1161
|
+
|
|
1162
|
+
## License
|
|
1163
|
+
|
|
1164
|
+
Apache-2.0
|
|
1165
|
+
|
|
1166
|
+
## Upgrading to 0.5
|
|
1167
|
+
|
|
1168
|
+
Replace segmentstream.semantic with typed metric queries of warehouse-backed entities. See [ontology metrics](../../docs/ontology-metrics.md). Warehouse-backed clients now also expose read-only `find`, `query` and `get`, with opaque record IDs; actions and stored history remain unavailable. See [warehouse record reads](../../docs/ontology-record-reads.md).
|
|
1169
|
+
|
|
1170
|
+
## Upgrading to 0.10
|
|
1171
|
+
|
|
1172
|
+
- Per-entity metric queries accept relationship paths. Their responses may carry
|
|
1173
|
+
`execution.warnings`; historical responses also carried `execution.sources`.
|
|
1174
|
+
- The model-level metric query is new, and its name is reserved: no entity is named after it.
|
|
1175
|
+
- The per-entity metric query now runs as the signed-in user through the ontology service, like
|
|
1176
|
+
record reads, and no longer uses `SEGMENTSTREAM_APP_CREDENTIAL`. The user needs read
|
|
1177
|
+
permission on the entities it reaches.
|
|
1178
|
+
- Server code without a signed-in user, such as a scheduled Route Handler, can no longer
|
|
1179
|
+
query metrics with the workspace credential. Use `segmentstream.warehouse` there, or call the
|
|
1180
|
+
ontology service with an API key.
|
|
1181
|
+
- Deploy the workspace's ontology with `@segmentstream/ontology` 0.13.0 or later. It
|
|
1182
|
+
serves the per-entity and model-level metric query routes; earlier versions don't.
|
|
1183
|
+
|
|
1184
|
+
## Upgrading to 0.13
|
|
1185
|
+
|
|
1186
|
+
- The model-level filter type, now `MetricQueryFilter`, is a union: a filter with `value` or
|
|
1187
|
+
`values`, or an `inPeriod` filter whose `value` is a `RelativePeriod`. An interface cannot
|
|
1188
|
+
extend a union, so `interface X extends MetricQueryFilter` does not compile; write
|
|
1189
|
+
`type X = MetricQueryFilter & { … }` instead. Reading `filter.values` still compiles: it is
|
|
1190
|
+
always `undefined` on an `inPeriod` filter.
|
|
1191
|
+
- The new operators, `inPeriod`, granularities and `timezone` need the workspace's ontology
|
|
1192
|
+
deployed with `@segmentstream/ontology` 0.17.0 or later. Requests that use none of
|
|
1193
|
+
them work as before.
|
|
1194
|
+
|
|
1195
|
+
## Request timing
|
|
1196
|
+
|
|
1197
|
+
Wrap app Route Handlers to add `Server-Timing: app;dur=…`, `x-request-id` and safe
|
|
1198
|
+
structured `app.request` runtime logs:
|
|
1199
|
+
|
|
1200
|
+
```ts
|
|
1201
|
+
import { withRequestTiming } from "@segmentstream/app";
|
|
1202
|
+
|
|
1203
|
+
export const GET = withRequestTiming(async (request: Request) => {
|
|
1204
|
+
return Response.json({ ok: true });
|
|
1205
|
+
});
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
The wrapper passes handler arguments through and preserves response headers and
|
|
1209
|
+
streaming bodies. It logs duration, method, HTTP status and a generated request ID,
|
|
1210
|
+
excluding URLs, cookies, bodies and error details. Errors propagate unchanged.
|
|
1211
|
+
Next.js redirects and `notFound()` log their HTTP status and propagate unchanged;
|
|
1212
|
+
other Next.js control-flow exceptions log `status: null` because Next owns their eventual response.
|
|
1213
|
+
Those exceptions cannot receive timing headers here;
|
|
1214
|
+
return an explicit response when the wrapper needs to decorate its headers.
|
|
1215
|
+
Read these logs with `segmentstream logs --service app` (deployment permission required).
|
|
1216
|
+
Duration ends when the handler returns response headers; it excludes later stream
|
|
1217
|
+
work and browser rendering. It is opt-in for Route Handlers, not a page renderer
|
|
1218
|
+
or a web vitals collector. A Next.js proxy cannot time the downstream page render.
|
|
1219
|
+
|
|
1220
|
+
Ontology SDK 0.31.2 adds request timing for ontology calls automatically. Timings
|
|
1221
|
+
are available in hosted logs; retained diagnostics are tracked in issue #520.
|
|
1222
|
+
|
|
1223
|
+
## Upgrading to 0.14
|
|
1224
|
+
|
|
1225
|
+
- The model-level metric query accepts model metrics by their bare names, typed from an ontology
|
|
1226
|
+
that declares them. Ontologies without them type the same names as before.
|
|
1227
|
+
- The field type, now `MetricQueryField`, gains `inputs`, which only fields of model metrics
|
|
1228
|
+
carry.
|
|
1229
|
+
|
|
1230
|
+
### Replication lifecycle and freshness
|
|
1231
|
+
|
|
1232
|
+
Cloud publishes a generation's views after its baseline, backfill and replay preparation finish
|
|
1233
|
+
and Pub/Sub accepts its fixed initialization prefix. Later writes do not postpone that boundary
|
|
1234
|
+
or make published views unavailable. Pub/Sub acceptance does not prove BigQuery visibility;
|
|
1235
|
+
the native BigQuery subscription handles delivery and retries. Replication status reports
|
|
1236
|
+
`captured` and `accepted`, and metric query responses carry no destination freshness watermark.
|
|
1237
|
+
Initial copies and replay remain unavailable until acceptance and view publication finish.
|