@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.
Files changed (125) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +2 -0
  3. package/README.md +1236 -2
  4. package/dist/agent.d.ts +56 -0
  5. package/dist/agent.js +203 -0
  6. package/dist/app.d.ts +82 -0
  7. package/dist/app.js +434 -0
  8. package/dist/browser-auth-capability.d.ts +2 -0
  9. package/dist/browser-auth-capability.js +2 -0
  10. package/dist/browser-revision.d.ts +19 -0
  11. package/dist/browser-revision.js +140 -0
  12. package/dist/channel-protocol.d.ts +16 -0
  13. package/dist/channel-protocol.js +17 -0
  14. package/dist/channel-session.d.ts +22 -0
  15. package/dist/channel-session.js +89 -0
  16. package/dist/config.d.ts +53 -0
  17. package/dist/config.js +112 -0
  18. package/dist/context.d.ts +10 -0
  19. package/dist/context.js +1 -0
  20. package/dist/cookies.d.ts +56 -0
  21. package/dist/cookies.js +131 -0
  22. package/dist/data.d.ts +233 -0
  23. package/dist/data.js +206 -0
  24. package/dist/decision-pages.d.ts +14 -0
  25. package/dist/decision-pages.js +87 -0
  26. package/dist/deployment.d.ts +30 -0
  27. package/dist/deployment.js +72 -0
  28. package/dist/errors.d.ts +60 -0
  29. package/dist/errors.js +123 -0
  30. package/dist/federation.d.ts +24 -0
  31. package/dist/federation.js +613 -0
  32. package/dist/groups.d.ts +95 -0
  33. package/dist/groups.js +280 -0
  34. package/dist/handlers.d.ts +10 -0
  35. package/dist/handlers.js +161 -0
  36. package/dist/index.d.ts +46 -0
  37. package/dist/index.js +29 -0
  38. package/dist/jobs.d.ts +49 -0
  39. package/dist/jobs.js +40 -0
  40. package/dist/legacy.d.ts +77 -0
  41. package/dist/legacy.js +57 -0
  42. package/dist/logo.d.ts +1 -0
  43. package/dist/logo.js +25 -0
  44. package/dist/me.d.ts +42 -0
  45. package/dist/me.js +54 -0
  46. package/dist/navigation/build.d.ts +68 -0
  47. package/dist/navigation/build.js +290 -0
  48. package/dist/navigation/cli.d.ts +19 -0
  49. package/dist/navigation/cli.js +37 -0
  50. package/dist/navigation/generate.d.ts +43 -0
  51. package/dist/navigation/generate.js +138 -0
  52. package/dist/navigation/next.d.ts +10 -0
  53. package/dist/navigation/next.js +18 -0
  54. package/dist/navigation-protocol.d.ts +55 -0
  55. package/dist/navigation-protocol.js +298 -0
  56. package/dist/neon.d.ts +101 -0
  57. package/dist/neon.js +539 -0
  58. package/dist/oauth-completion.d.ts +6 -0
  59. package/dist/oauth-completion.js +106 -0
  60. package/dist/oauth.d.ts +59 -0
  61. package/dist/oauth.js +129 -0
  62. package/dist/ontology-names.d.ts +10 -0
  63. package/dist/ontology-names.js +38 -0
  64. package/dist/ontology-transport.d.ts +34 -0
  65. package/dist/ontology-transport.js +130 -0
  66. package/dist/ontology-wire.d.ts +66 -0
  67. package/dist/ontology-wire.js +355 -0
  68. package/dist/ontology.d.ts +381 -0
  69. package/dist/ontology.js +367 -0
  70. package/dist/pages.d.ts +6 -0
  71. package/dist/pages.js +6 -0
  72. package/dist/parameters.d.ts +55 -0
  73. package/dist/parameters.js +118 -0
  74. package/dist/permissions.d.ts +19 -0
  75. package/dist/permissions.js +55 -0
  76. package/dist/pkce.d.ts +8 -0
  77. package/dist/pkce.js +40 -0
  78. package/dist/proxy.d.ts +25 -0
  79. package/dist/proxy.js +135 -0
  80. package/dist/request-timing.d.ts +6 -0
  81. package/dist/request-timing.js +53 -0
  82. package/dist/return-to.d.ts +2 -0
  83. package/dist/return-to.js +24 -0
  84. package/dist/server.d.ts +11 -0
  85. package/dist/server.js +80 -0
  86. package/dist/sessions.d.ts +27 -0
  87. package/dist/sessions.js +72 -0
  88. package/dist/sign-in.d.ts +37 -0
  89. package/dist/sign-in.js +95 -0
  90. package/dist/storage.d.ts +79 -0
  91. package/dist/storage.js +180 -0
  92. package/dist/telegram-launch.d.ts +7 -0
  93. package/dist/telegram-launch.js +134 -0
  94. package/dist/telegram.d.ts +2 -0
  95. package/dist/telegram.js +6 -0
  96. package/dist/tokens.d.ts +51 -0
  97. package/dist/tokens.js +70 -0
  98. package/dist/ui/agent-connect.d.ts +18 -0
  99. package/dist/ui/agent-connect.js +23 -0
  100. package/dist/ui/components.d.ts +27 -0
  101. package/dist/ui/components.js +71 -0
  102. package/dist/ui/hooks.d.ts +19 -0
  103. package/dist/ui/hooks.js +56 -0
  104. package/dist/ui/index.d.ts +22 -0
  105. package/dist/ui/index.js +15 -0
  106. package/dist/ui/navigation.d.ts +5 -0
  107. package/dist/ui/navigation.js +8 -0
  108. package/dist/ui/segmentstream-logo.d.ts +2 -0
  109. package/dist/ui/segmentstream-logo.js +7 -0
  110. package/dist/ui/sign-in-check.d.ts +14 -0
  111. package/dist/ui/sign-in-check.js +41 -0
  112. package/dist/ui/sign-in.d.ts +17 -0
  113. package/dist/ui/sign-in.js +65 -0
  114. package/dist/ui/store.d.ts +80 -0
  115. package/dist/ui/store.js +280 -0
  116. package/dist/ui/styles.css +249 -0
  117. package/dist/ui/telegram-sign-in.d.ts +16 -0
  118. package/dist/ui/telegram-sign-in.js +21 -0
  119. package/dist/ui/use-browser-sign-in.d.ts +52 -0
  120. package/dist/ui/use-browser-sign-in.js +169 -0
  121. package/dist/ui/use-channel-sign-in.d.ts +10 -0
  122. package/dist/ui/use-channel-sign-in.js +118 -0
  123. package/dist/ui/use-sign-in.d.ts +109 -0
  124. package/dist/ui/use-sign-in.js +304 -0
  125. package/package.json +95 -4
package/README.md CHANGED
@@ -1,3 +1,1237 @@
1
- # Temporary Holding Version
1
+ # @segmentstream/app
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.