@segmentstream-cloud/app 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -347,16 +347,19 @@ export const { POST } = segmentstream.agent.connectHandler();
347
347
 
348
348
  `segmentstream.agent` holds:
349
349
 
350
- | Call | Does |
351
- | ---------------------- | ------------------------------------------------------------------------------- |
352
- | `pairing(code)` | `{ provider, display, expiresAt }`; throws `not_found` or `expired` |
353
- | `approvePairing(code)` | Links the account to the signed-in member; returns the `AgentIdentity` |
354
- | `identities()` | The member's own: `{ id, provider, externalId, display, createdAt, revokedAt }` |
355
- | `revokeIdentity(id)` | Revokes one of them at once |
356
- | `connectHandler()` | `{ POST }` for the page's form: same-origin only, takes `code`, answers `303` |
350
+ | Call | Does |
351
+ | ---------------------- | -------------------------------------------------------------------------------------- |
352
+ | `pairing(code)` | `{ provider, display, expiresAt }`; throws `not_found` or `expired` |
353
+ | `approvePairing(code)` | Links the account to the signed-in member; returns the `AgentIdentity` |
354
+ | `identities()` | The member's own: `{ id, provider, externalId, display, createdAt, revokedAt }` |
355
+ | `revokeIdentity(id)` | Revokes one of them at once |
356
+ | `connectHandler()` | `{ POST }` for the page's form: same-origin only; `303` on success; throws on conflict |
357
357
 
358
358
  The handler sends the browser back to `/agent/connect?connected=<provider>`, or to
359
- `/agent/connect?expired=1` when the code was used or is older than ten minutes. Other
359
+ `/agent/connect?expired=1` when the code was used or is older than ten minutes. An account already
360
+ connected to another member throws `SegmentStreamApiError` with status `409` and
361
+ `code === "identity_in_use"` through the app's existing error mechanism. The app owns conflict copy.
362
+ Same-member pairing is idempotent, and replacing your own previous account on a provider is supported. Other
360
363
  refusals, such as `forbidden` for a member the ontology does not admit, throw
361
364
  `SegmentStreamApiError`. All calls are server-only and run as the signed-in member;
362
365
  the ontology decides every one. The workspace starter renders the page with Tailwind CSS
@@ -698,6 +701,34 @@ Replace segmentstream.semantic with typed warehouse-backed entity aggregates. Se
698
701
  deployed with `@segmentstream-cloud/ontology` 0.17.0 or later. Requests that use none of
699
702
  them work as before.
700
703
 
704
+ ## Request timing
705
+
706
+ Wrap app Route Handlers to add `Server-Timing: app;dur=…`, `x-request-id` and safe
707
+ structured `app.request` runtime logs:
708
+
709
+ ```ts
710
+ import { withRequestTiming } from "@segmentstream-cloud/app";
711
+
712
+ export const GET = withRequestTiming(async (request: Request) => {
713
+ return Response.json({ ok: true });
714
+ });
715
+ ```
716
+
717
+ The wrapper passes handler arguments through and preserves response headers and
718
+ streaming bodies. It logs duration, method, HTTP status and a generated request ID,
719
+ excluding URLs, cookies, bodies and error details. Errors propagate unchanged.
720
+ Next.js redirects and `notFound()` log their HTTP status and propagate unchanged;
721
+ other Next.js control-flow exceptions log `status: null` because Next owns their eventual response.
722
+ Those exceptions cannot receive timing headers here;
723
+ return an explicit response when the wrapper needs to decorate its headers.
724
+ Read these logs with `segmentstream logs --service app` (deployment permission required).
725
+ Duration ends when the handler returns response headers; it excludes later stream
726
+ work and browser rendering. It is opt-in for Route Handlers, not a page renderer
727
+ or a web vitals collector. A Next.js proxy cannot time the downstream page render.
728
+
729
+ Ontology SDK 0.31.2 adds request timing for ontology calls automatically. Timings
730
+ are available in hosted logs; retained diagnostics are tracked in issue #520.
731
+
701
732
  ## Upgrading to 0.14
702
733
 
703
734
  - `segmentstream.ontology.aggregate()` accepts the measures of the model by their bare names, typed
package/dist/agent.d.ts CHANGED
@@ -34,7 +34,8 @@ export interface AgentIdentities {
34
34
  readonly pairing: (code: string) => Promise<AgentPairing>;
35
35
  /**
36
36
  * Links the pairing's chat account to the signed-in member. The ontology decides: it refuses a
37
- * used or expired code (`not_found`, `expired`) and a member it does not admit (`forbidden`).
37
+ * used or expired code (`not_found`, `expired`), a member it does not admit (`forbidden`), or an
38
+ * account linked to another member (`identity_in_use`, HTTP 409).
38
39
  * Server-only.
39
40
  */
40
41
  readonly approvePairing: (code: string) => Promise<AgentIdentity>;
package/dist/agent.js CHANGED
@@ -9,6 +9,7 @@ const GONE_CODES = new Set(["not_found", "expired"]);
9
9
  const STATUS_CODES = {
10
10
  403: "forbidden",
11
11
  404: "not_found",
12
+ 409: "identity_in_use",
12
13
  410: "expired",
13
14
  };
14
15
  function pairingPath(code, approve) {
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export declare function createSegmentStreamApp<O = unknown>(config: SegmentStrea
18
18
  export { NOT_INVITED, SIGN_IN_LINK_INVALID, SegmentStreamApiError } from "./errors.js";
19
19
  export { grants, isAllowed } from "./permissions.js";
20
20
  export { sql } from "./parameters.js";
21
+ export { withRequestTiming } from "./request-timing.js";
21
22
  export type { AuthenticationMethod, SegmentStreamApp } from "./app.js";
22
23
  export { CONSENT_PAGE } from "./oauth.js";
23
24
  export { AGENT_CONNECT_HANDLER, AGENT_CONNECT_PAGE } from "./agent.js";
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ export function createSegmentStreamApp(config) {
19
19
  export { NOT_INVITED, SIGN_IN_LINK_INVALID, SegmentStreamApiError } from "./errors.js";
20
20
  export { grants, isAllowed } from "./permissions.js";
21
21
  export { sql } from "./parameters.js";
22
+ export { withRequestTiming } from "./request-timing.js";
22
23
  export { CONSENT_PAGE } from "./oauth.js";
23
24
  export { AGENT_CONNECT_HANDLER, AGENT_CONNECT_PAGE } from "./agent.js";
24
25
  export { createBlobStorageClient, } from "./storage.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Time an app Route Handler until it returns response headers. Streaming/rendering after that
3
+ * point is excluded. Logs contain no URL, cookies, body or error text. Server Components need
4
+ * framework instrumentation for complete page timing; a Next.js proxy only times authentication.
5
+ */
6
+ export declare function withRequestTiming<R extends Request, A extends unknown[]>(handler: (request: R, ...args: A) => Promise<Response>): (request: R, ...args: A) => Promise<Response>;
@@ -0,0 +1,53 @@
1
+ import { unstable_rethrow } from "next/navigation";
2
+ import { getRedirectStatusCodeFromError } from "next/dist/client/components/redirect.js";
3
+ import { isRedirectError } from "next/dist/client/components/redirect-error.js";
4
+ import { getAccessFallbackHTTPStatus, isHTTPAccessFallbackError, } from "next/dist/client/components/http-access-fallback/http-access-fallback.js";
5
+ /**
6
+ * Time an app Route Handler until it returns response headers. Streaming/rendering after that
7
+ * point is excluded. Logs contain no URL, cookies, body or error text. Server Components need
8
+ * framework instrumentation for complete page timing; a Next.js proxy only times authentication.
9
+ */
10
+ export function withRequestTiming(handler) {
11
+ return async (request, ...args) => {
12
+ const started = performance.now();
13
+ const requestId = crypto.randomUUID();
14
+ let status = 500;
15
+ let durationMs;
16
+ try {
17
+ const response = await handler(request, ...args);
18
+ const headers = new Headers(response.headers);
19
+ durationMs = Math.round((performance.now() - started) * 100) / 100;
20
+ status = response.status;
21
+ headers.append("server-timing", `app;dur=${durationMs}`);
22
+ headers.set("x-request-id", requestId);
23
+ return new Response(response.body, {
24
+ status,
25
+ statusText: response.statusText,
26
+ headers,
27
+ });
28
+ }
29
+ catch (error) {
30
+ try {
31
+ unstable_rethrow(error);
32
+ }
33
+ catch {
34
+ // Next owns the eventual response for control-flow exceptions, outside this wrapper.
35
+ status = isRedirectError(error)
36
+ ? getRedirectStatusCodeFromError(error)
37
+ : isHTTPAccessFallbackError(error)
38
+ ? getAccessFallbackHTTPStatus(error)
39
+ : null;
40
+ }
41
+ throw error;
42
+ }
43
+ finally {
44
+ console.info(JSON.stringify({
45
+ event: "app.request",
46
+ requestId,
47
+ method: request.method,
48
+ status,
49
+ durationMs: durationMs ?? Math.round((performance.now() - started) * 100) / 100,
50
+ }));
51
+ }
52
+ };
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream-cloud/app",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "description": "Sign-in, users, permissions, the ontology client and warehouse data access for SegmentStream workspace apps built with Next.js.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SegmentStream",