@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/dist/data.d.ts ADDED
@@ -0,0 +1,233 @@
1
+ import type { ResolvedConfig } from "./config.js";
2
+ import type { QueryParameters } from "./parameters.js";
3
+ import type { Fetch } from "./sessions.js";
4
+ export { SegmentStreamApiError } from "./errors.js";
5
+ export type WarehouseValue = string | number | boolean | null;
6
+ export type WarehouseRow = Record<string, WarehouseValue>;
7
+ export interface WarehouseColumn {
8
+ readonly name: string;
9
+ readonly type: string;
10
+ readonly mode: string;
11
+ readonly description: string;
12
+ }
13
+ /** How much of a query result to wait for and collect. */
14
+ export interface WarehouseReadOptions {
15
+ /** Rows to collect across pages before stopping. Defaults to 10,000. */
16
+ readonly maxRows?: number;
17
+ /** Rows per API page, 1–1,000. Defaults to the smaller of `maxRows` and 1,000. */
18
+ readonly pageSize?: number;
19
+ /** How long to wait for a running query. Defaults to 60 seconds. */
20
+ readonly timeoutMs?: number;
21
+ readonly signal?: AbortSignal;
22
+ }
23
+ export interface WarehouseQueryOptions extends WarehouseReadOptions {
24
+ /** Values for named `@parameters` in the SQL. Never interpolate values into the SQL text. */
25
+ readonly params?: QueryParameters;
26
+ /** Lowers the workspace's per-query byte ceiling for this query. */
27
+ readonly maximumBytesBilled?: number | bigint | string;
28
+ }
29
+ export interface WarehouseQueryResult<Row = WarehouseRow> {
30
+ readonly queryId: string | null;
31
+ readonly columns: readonly WarehouseColumn[];
32
+ readonly rows: Row[];
33
+ /** Rows in the full result, which may exceed `rows` when `truncated`. */
34
+ readonly totalRows: number | null;
35
+ /** True when more rows exist than `maxRows` allowed. */
36
+ readonly truncated: boolean;
37
+ readonly bytesProcessed: string | null;
38
+ readonly bytesBilled: string | null;
39
+ readonly cacheHit: boolean | null;
40
+ }
41
+ export interface WarehouseDryRun {
42
+ readonly columns: readonly WarehouseColumn[];
43
+ readonly bytesProcessed: string | null;
44
+ readonly maximumBytesBilled: string;
45
+ }
46
+ /**
47
+ * The filter operators of both metric queries. `equals` and `notEquals` take one `value`; `in` and
48
+ * `notIn` take 1–100 `values`; `isNull` and `isNotNull` take none. `notEquals` and `notIn` also
49
+ * match nulls and rows whose relationship found no target, so they are the exact complements of
50
+ * `equals` and `in`. `gt`, `gte`, `lt` and `lte` compare a date, timestamp or number dimension
51
+ * with one `value`; a date bound on a timestamp covers whole days. `contains`, `startsWith` and
52
+ * `endsWith` match one non-empty string in a text dimension, case-sensitively, without wildcards.
53
+ */
54
+ export type MetricQueryFilterOperator = "equals" | "in" | "notEquals" | "notIn" | "isNull" | "isNotNull" | "gt" | "gte" | "lt" | "lte" | "contains" | "startsWith" | "endsWith";
55
+ /** How a date or timestamp dimension is bucketed. `hour` needs a timestamp and yields one. */
56
+ export type MetricQueryGranularity = "hour" | "day" | "week" | "month" | "quarter" | "year";
57
+ type CalendarUnit = "week" | "month" | "quarter" | "year";
58
+ /**
59
+ * A period relative to today, which the platform resolves to two inclusive dates: `today`,
60
+ * `yesterday`; the whole current or previous week, month, quarter or year (`this month`,
61
+ * `last quarter`); the current one through today (`this year to date`); the N days ending
62
+ * yesterday (`last 7 days`); or the N whole weeks, months, quarters or years before the current
63
+ * one (`last 4 weeks`). N is at least 1, weeks start on Monday, and a period covers at most 3660
64
+ * days.
65
+ */
66
+ export type RelativePeriod = "today" | "yesterday" | `this ${CalendarUnit}` | `this ${CalendarUnit} to date` | `last ${CalendarUnit}` | `last ${number} ${"days" | "weeks" | "months" | "quarters" | "years"}`;
67
+ /**
68
+ * A metric query of one entity: its metrics, grouped by dimensions. Dimensions, filters and
69
+ * ordering name the entity's own properties, or a related entity's property through to-one
70
+ * relationships: `Path`, such as `product.brand`.
71
+ */
72
+ export interface EntityMetricQuery<Property extends string = string, Metric extends string = string, Temporal extends Property = Property, Path extends string = never> {
73
+ readonly dimensions: readonly (Property | Path)[];
74
+ readonly metrics: readonly Metric[];
75
+ /** The only partition bound this request takes: it has neither `between` nor `inPeriod`. */
76
+ readonly dateRange?: {
77
+ readonly from: string;
78
+ readonly to: string;
79
+ };
80
+ /** The selected date or timestamp dimension to bucket, which may be a related one. */
81
+ readonly granularityOf?: Temporal | Path;
82
+ readonly granularity?: MetricQueryGranularity;
83
+ /**
84
+ * An IANA timezone, such as `Europe/Moscow`, or `UTC`. It replaces the entities' timezones for
85
+ * timestamp buckets and date bounds on timestamps; metric conditions keep their own.
86
+ */
87
+ readonly timezone?: string;
88
+ readonly filters?: ReadonlyArray<{
89
+ readonly dimension: Property | Path;
90
+ readonly operator: MetricQueryFilterOperator;
91
+ readonly value?: string | number | boolean;
92
+ readonly values?: ReadonlyArray<string | number | boolean>;
93
+ }>;
94
+ readonly orderBy?: ReadonlyArray<{
95
+ readonly metric?: Metric;
96
+ readonly dimension?: Property | Path;
97
+ readonly direction: "asc" | "desc";
98
+ }>;
99
+ readonly limit?: number;
100
+ }
101
+ /**
102
+ * A filter of a model-level metric query. Besides the shared operators, `between` takes two
103
+ * inclusive `values`, the lower first, and `inPeriod` a period relative to today as its `value`;
104
+ * the answer's `execution.periods` gives the dates it resolved to. Only these two bound a
105
+ * partitioned entity.
106
+ */
107
+ export type MetricQueryFilter<Dimension extends string = string> = {
108
+ readonly dimension: Dimension;
109
+ readonly operator: MetricQueryFilterOperator | "between";
110
+ readonly value?: string | number | boolean;
111
+ readonly values?: ReadonlyArray<string | number | boolean>;
112
+ } | {
113
+ readonly dimension: Dimension;
114
+ readonly operator: "inPeriod";
115
+ readonly value: RelativePeriod;
116
+ /** Never set; declared so code reading `values` from any filter still compiles. */
117
+ readonly values?: undefined;
118
+ };
119
+ /**
120
+ * A model-level metric query: metrics of one or more entities, as `entity.metric`, and model
121
+ * metrics, declared in `defineOntology({ metrics })`, by their bare names, grouped by
122
+ * `entity.property` dimensions. A dimension may instead be a route from a metric's entity,
123
+ * `entity.relationship.property`, which picks the way that entity reaches the property. Date bounds
124
+ * are `between` or `inPeriod` filters.
125
+ */
126
+ export interface MetricQuery<Metric extends string = string, Dimension extends string = string> {
127
+ readonly metrics: readonly Metric[];
128
+ readonly dimensions?: readonly Dimension[];
129
+ readonly filters?: ReadonlyArray<MetricQueryFilter<Dimension>>;
130
+ readonly granularity?: MetricQueryGranularity;
131
+ /** The requested date or timestamp dimension to bucket by `granularity`. */
132
+ readonly granularityOf?: Dimension;
133
+ /**
134
+ * An IANA timezone, such as `Europe/Moscow`, or `UTC`. It replaces the entities' timezones for
135
+ * timestamp buckets, date bounds on timestamps and the today of `inPeriod`; metric conditions
136
+ * keep their own.
137
+ */
138
+ readonly timezone?: string;
139
+ readonly orderBy?: ReadonlyArray<{
140
+ readonly metric?: Metric;
141
+ readonly dimension?: Dimension;
142
+ readonly direction: "asc" | "desc";
143
+ }>;
144
+ /** Rows to return, 1–1000; 100 by default. */
145
+ readonly limit?: number;
146
+ }
147
+ /** A column of a metric query's answer: a dimension or a metric. */
148
+ export interface MetricQueryField {
149
+ readonly name: string;
150
+ readonly label: string;
151
+ readonly type: string;
152
+ readonly unit?: string;
153
+ readonly format?: string;
154
+ /** On a metric: the filters applied to it. A filter its entity cannot reach is not listed. */
155
+ readonly filters?: readonly string[];
156
+ /** `false` on a metric whose rows don't add up to a total, such as a distinct count or ratio. */
157
+ readonly additive?: boolean;
158
+ /**
159
+ * On a balance metric: the date or timestamp dimension, as `entity.property`, whose days its
160
+ * rows snapshot. The request groups by it or filters it to one day.
161
+ */
162
+ readonly balanceOver?: string;
163
+ /**
164
+ * On a model metric: the entity metrics it reads, as `entity.metric`, in declaration order.
165
+ * They are computed whether or not the request names them, and come back as columns only when
166
+ * it does. Its `filters` are the filters that applied to every one of them.
167
+ */
168
+ readonly inputs?: readonly string[];
169
+ }
170
+ /** The answer to a metric query, per entity or model-level. */
171
+ export interface MetricQueryResult<Row = Record<string, string | boolean | null>> {
172
+ readonly deploymentId: string | null;
173
+ readonly definitionDigest: string;
174
+ readonly fields: readonly MetricQueryField[];
175
+ readonly rows: Row[];
176
+ readonly truncated: boolean;
177
+ readonly execution: {
178
+ readonly elapsedMs: number;
179
+ readonly bytesProcessed?: string;
180
+ readonly bytesBilled?: string;
181
+ /**
182
+ * Data problems the query met, for example a join key that matched more than one record, and
183
+ * filters that did not apply to some metric's entity.
184
+ */
185
+ readonly warnings?: readonly string[];
186
+ /** The stored entities a query read, each with the time up to which its copy is complete. */
187
+ /** Historical server observations; current servers omit destination freshness metadata. */
188
+ readonly sources?: ReadonlyArray<{
189
+ readonly entity: string;
190
+ readonly asOf: string;
191
+ }>;
192
+ /**
193
+ * Each `inPeriod` filter in request order: its dimension as requested, its period, the
194
+ * inclusive dates it resolved to and the timezone whose today decided them.
195
+ */
196
+ readonly periods?: ReadonlyArray<{
197
+ readonly dimension: string;
198
+ readonly value: string;
199
+ readonly from: string;
200
+ readonly to: string;
201
+ readonly timezone: string;
202
+ }>;
203
+ };
204
+ }
205
+ export interface WarehouseClient {
206
+ /** Runs read-only SQL in the workspace warehouse as the signed-in user's app session. */
207
+ readonly query: <Row = WarehouseRow>(sql: string, options?: WarehouseQueryOptions) => Promise<WarehouseQueryResult<Row>>;
208
+ /**
209
+ * Resumes a query by ID from a later request. Queries keep running in the warehouse for
210
+ * up to an hour after `query()` stops waiting, and each request uses a fresh app token.
211
+ */
212
+ readonly result: <Row = WarehouseRow>(queryId: string, options?: WarehouseReadOptions) => Promise<WarehouseQueryResult<Row>>;
213
+ /** Validates SQL and estimates bytes processed without running it. */
214
+ readonly dryRun: (sql: string, options?: Pick<WarehouseQueryOptions, "params" | "maximumBytesBilled" | "signal">) => Promise<WarehouseDryRun>;
215
+ }
216
+ export interface DataClientDependencies {
217
+ readonly config: ResolvedConfig;
218
+ readonly fetch: Fetch;
219
+ readonly getAccessToken: () => Promise<string | null>;
220
+ /**
221
+ * The workspace app credential, `SEGMENTSTREAM_APP_CREDENTIAL`, read on every call. The API
222
+ * accepts it for warehouse queries and opted-in job runs; otherwise they use the session token.
223
+ */
224
+ readonly getAppCredential?: () => string | undefined;
225
+ readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
226
+ readonly now?: () => number;
227
+ }
228
+ /** Server-side SegmentStream API access as the signed-in member. */
229
+ export declare function createDataClient(dependencies: DataClientDependencies): {
230
+ fetch: (path: string, init?: RequestInit) => Promise<Response>;
231
+ warehouse: WarehouseClient;
232
+ jobs: import("./jobs.js").JobsClient;
233
+ };
package/dist/data.js ADDED
@@ -0,0 +1,206 @@
1
+ import { SegmentStreamApiError, readJson } from "./errors.js";
2
+ import { encodeParameters } from "./parameters.js";
3
+ import { createJobsClient } from "./jobs.js";
4
+ export { SegmentStreamApiError } from "./errors.js";
5
+ const REQUEST_TIMEOUT_MS = 45_000;
6
+ const DEFAULT_MAX_ROWS = 10_000;
7
+ const MAXIMUM_PAGE_SIZE = 1000;
8
+ const DEFAULT_TIMEOUT_MS = 60_000;
9
+ const FIRST_POLL_MS = 500;
10
+ const LONGEST_POLL_MS = 2000;
11
+ /** Server-side SegmentStream API access as the signed-in member. */
12
+ export function createDataClient(dependencies) {
13
+ const { config } = dependencies;
14
+ const workspace = `/api/v1/workspaces/${encodeURIComponent(config.workspaceId)}`;
15
+ const sleep = dependencies.sleep ?? defaultSleep;
16
+ const now = dependencies.now ?? Date.now;
17
+ /** Calls any SegmentStream API path as the signed-in member; the token never leaves the server. */
18
+ function apiFetch(path, init = {}) {
19
+ return authorizedFetch(path, init, dependencies.getAccessToken);
20
+ }
21
+ /** Workspace operations prefer the app credential, which needs no SegmentStream session. */
22
+ async function workspaceToken() {
23
+ return dependencies.getAppCredential?.() ?? dependencies.getAccessToken();
24
+ }
25
+ async function authorizedFetch(path, init, getToken) {
26
+ if (!path.startsWith("/api/")) {
27
+ throw new TypeError('SegmentStream API paths start with "/api/", for example "/api/v1/me".');
28
+ }
29
+ const token = await getToken();
30
+ if (token === null) {
31
+ throw new SegmentStreamApiError({
32
+ code: "app_session_missing",
33
+ status: 401,
34
+ detail: "No signed-in SegmentStream session for this request; call SegmentStream from Server Components, Route Handlers, or Server Functions behind the SegmentStream proxy.",
35
+ });
36
+ }
37
+ const headers = new Headers(init.headers);
38
+ const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
39
+ headers.set("authorization", `Bearer ${token}`);
40
+ if (!headers.has("accept")) {
41
+ headers.set("accept", "application/json");
42
+ }
43
+ try {
44
+ return await dependencies.fetch(`${config.baseUrl}${path}`, {
45
+ ...init,
46
+ headers,
47
+ cache: "no-store",
48
+ redirect: "error",
49
+ signal: init.signal ? AbortSignal.any([init.signal, timeout]) : timeout,
50
+ });
51
+ }
52
+ catch (error) {
53
+ if (init.signal?.aborted) {
54
+ throw error;
55
+ }
56
+ throw new SegmentStreamApiError({
57
+ code: "segmentstream_unreachable",
58
+ status: 503,
59
+ detail: "SegmentStream could not be reached.",
60
+ retryable: true,
61
+ });
62
+ }
63
+ }
64
+ async function call(method, path, body, signal, getToken = dependencies.getAccessToken) {
65
+ const response = await authorizedFetch(path, {
66
+ method,
67
+ ...(body === undefined
68
+ ? {}
69
+ : { body: JSON.stringify(body), headers: { "content-type": "application/json" } }),
70
+ ...(signal ? { signal } : {}),
71
+ }, getToken);
72
+ const payload = await readJson(response);
73
+ if (!response.ok) {
74
+ throw errorFrom(response.status, payload);
75
+ }
76
+ return payload;
77
+ }
78
+ function limits(options) {
79
+ const maxRows = positiveInteger(options.maxRows ?? DEFAULT_MAX_ROWS, "maxRows");
80
+ const pageSize = positiveInteger(options.pageSize ?? Math.min(maxRows, MAXIMUM_PAGE_SIZE), "pageSize");
81
+ return { maxRows, pageSize, deadline: now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS) };
82
+ }
83
+ /** Waits for a running query, then follows pages up to `maxRows`. */
84
+ async function collect(first, { maxRows, pageSize, deadline }, signal) {
85
+ let page = first;
86
+ const queryId = page.id ?? null;
87
+ let wait = FIRST_POLL_MS;
88
+ while (page.state === "running") {
89
+ if (queryId === null || now() + wait > deadline) {
90
+ throw new SegmentStreamApiError({
91
+ code: "query_timeout",
92
+ status: 504,
93
+ detail: "The query is still running; resume it from a later request with warehouse.result(queryId).",
94
+ retryable: true,
95
+ queryId: queryId ?? undefined,
96
+ });
97
+ }
98
+ await sleep(wait, signal);
99
+ wait = Math.min(wait * 2, LONGEST_POLL_MS);
100
+ page = (await read(queryId, pageSize, undefined, signal)).query;
101
+ }
102
+ const head = page;
103
+ const rows = [...page.rows];
104
+ while (page.nextPageToken !== undefined && rows.length < maxRows && queryId !== null) {
105
+ page = (await read(queryId, pageSize, page.nextPageToken, signal)).query;
106
+ rows.push(...page.rows);
107
+ }
108
+ return {
109
+ queryId,
110
+ columns: head.columns,
111
+ rows: rows.slice(0, maxRows),
112
+ totalRows: head.totalRows === undefined ? null : Number(head.totalRows),
113
+ truncated: rows.length > maxRows || page.nextPageToken !== undefined,
114
+ bytesProcessed: head.bytesProcessed ?? null,
115
+ bytesBilled: head.bytesBilled ?? null,
116
+ cacheHit: head.cacheHit ?? null,
117
+ };
118
+ }
119
+ async function query(sql, options = {}) {
120
+ const bounds = limits(options);
121
+ const { query: page } = await call("POST", `${workspace}/warehouse/queries`, {
122
+ sql,
123
+ parameters: encodeParameters(options.params),
124
+ ...bytesLimit(options.maximumBytesBilled),
125
+ pageSize: bounds.pageSize,
126
+ }, options.signal, workspaceToken);
127
+ return collect(page, bounds, options.signal);
128
+ }
129
+ async function result(queryId, options = {}) {
130
+ const bounds = limits(options);
131
+ const { query: page } = await read(queryId, bounds.pageSize, undefined, options.signal);
132
+ return collect(page, bounds, options.signal);
133
+ }
134
+ function read(queryId, pageSize, pageToken, signal) {
135
+ const search = new URLSearchParams({ pageSize: String(pageSize) });
136
+ if (pageToken !== undefined) {
137
+ search.set("pageToken", pageToken);
138
+ }
139
+ return call("GET", `${workspace}/warehouse/queries/${encodeURIComponent(queryId)}?${search.toString()}`, undefined, signal, workspaceToken);
140
+ }
141
+ const warehouse = {
142
+ query,
143
+ result,
144
+ dryRun: async (sql, options = {}) => {
145
+ const { query: page } = await call("POST", `${workspace}/warehouse/queries`, {
146
+ sql,
147
+ parameters: encodeParameters(options.params),
148
+ ...bytesLimit(options.maximumBytesBilled),
149
+ dryRun: true,
150
+ }, options.signal, workspaceToken);
151
+ return {
152
+ columns: page.columns,
153
+ bytesProcessed: page.bytesProcessed ?? null,
154
+ maximumBytesBilled: page.maximumBytesBilled,
155
+ };
156
+ },
157
+ };
158
+ const jobs = createJobsClient({
159
+ workspace,
160
+ fetch: (path, init) => authorizedFetch(path, init, workspaceToken),
161
+ });
162
+ return { fetch: apiFetch, warehouse, jobs };
163
+ }
164
+ function bytesLimit(value) {
165
+ if (value === undefined) {
166
+ return {};
167
+ }
168
+ const text = String(value);
169
+ if (!/^[1-9][0-9]*$/.test(text)) {
170
+ throw new TypeError("maximumBytesBilled must be a positive whole number of bytes.");
171
+ }
172
+ return { maximumBytesBilled: text };
173
+ }
174
+ function positiveInteger(value, name) {
175
+ if (!Number.isInteger(value) || value < 1) {
176
+ throw new TypeError(`${name} must be a positive integer.`);
177
+ }
178
+ return value;
179
+ }
180
+ function errorFrom(status, payload) {
181
+ const body = typeof payload === "object" && payload !== null ? payload : {};
182
+ return new SegmentStreamApiError({
183
+ code: typeof body.error === "string" ? body.error : `http_${status}`,
184
+ status,
185
+ detail: typeof body.detail === "string" ? body.detail : "SegmentStream rejected the request.",
186
+ requestId: typeof body.requestId === "string" ? body.requestId : undefined,
187
+ retryable: typeof body.retryable === "boolean" ? body.retryable : status >= 500,
188
+ });
189
+ }
190
+ function defaultSleep(ms, signal) {
191
+ return new Promise((resolve, reject) => {
192
+ if (signal?.aborted) {
193
+ reject(signal.reason);
194
+ return;
195
+ }
196
+ const timer = setTimeout(() => {
197
+ signal?.removeEventListener("abort", abort);
198
+ resolve();
199
+ }, ms);
200
+ const abort = () => {
201
+ clearTimeout(timer);
202
+ reject(signal?.reason);
203
+ };
204
+ signal?.addEventListener("abort", abort, { once: true });
205
+ });
206
+ }
@@ -0,0 +1,14 @@
1
+ import { type SegmentStreamProxy } from "./proxy.js";
2
+ export declare function isDecisionPage(pathname: string): boolean;
3
+ /**
4
+ * The proxy, with the decision pages unframeable: no other site may show them in a frame and trick
5
+ * a member into clicking Allow or Approve. Every app gets this from the SDK, whatever its Next.js
6
+ * config says.
7
+ */
8
+ export declare function withUnframeablePages(proxy: SegmentStreamProxy, frameAncestors?: string): SegmentStreamProxy;
9
+ /**
10
+ * Whether a form post came from the app's own pages. A decision must never be made from another
11
+ * site, which could otherwise approve an agent in the member's name. Fetch Metadata describes
12
+ * the browser's public request even when a trusted proxy rewrites the server's request URL.
13
+ */
14
+ export declare function isSameOrigin(request: Request): boolean;
@@ -0,0 +1,87 @@
1
+ import { isLoopbackHostname } from "./cookies.js";
2
+ import { AGENT_CONNECT_PAGE, CONSENT_PAGE } from "./pages.js";
3
+ import { matchesPathPrefix } from "./proxy.js";
4
+ /** Pages where a signed-in member lets an agent act for them. */
5
+ const DECISION_PAGES = [CONSENT_PAGE, AGENT_CONNECT_PAGE, "/sign-in", "/api/oauth/consent"];
6
+ export function isDecisionPage(pathname) {
7
+ return DECISION_PAGES.some((page) => matchesPathPrefix(pathname, page));
8
+ }
9
+ /**
10
+ * The proxy, with the decision pages unframeable: no other site may show them in a frame and trick
11
+ * a member into clicking Allow or Approve. Every app gets this from the SDK, whatever its Next.js
12
+ * config says.
13
+ */
14
+ export function withUnframeablePages(proxy, frameAncestors) {
15
+ return async (request) => {
16
+ const response = await proxy(request);
17
+ const pathname = request.nextUrl.pathname;
18
+ if (isDecisionPage(pathname)) {
19
+ response.headers.set("cache-control", "no-store");
20
+ response.headers.set("referrer-policy", "no-referrer");
21
+ response.headers.set("x-frame-options", "DENY");
22
+ const policy = response.headers.get("content-security-policy");
23
+ response.headers.set("content-security-policy", policy === null
24
+ ? "frame-ancestors 'none'"
25
+ : policy.replace(/frame-ancestors[^;]*/g, "frame-ancestors 'none'") +
26
+ (policy.includes("frame-ancestors") ? "" : "; frame-ancestors 'none'"));
27
+ }
28
+ else if (frameAncestors &&
29
+ !["/api", "/mcp", "/.well-known", "/_next"].some((path) => matchesPathPrefix(pathname, path))) {
30
+ const policy = response.headers.get("content-security-policy");
31
+ const ancestors = `frame-ancestors ${frameAncestors}`;
32
+ response.headers.delete("x-frame-options");
33
+ response.headers.set("content-security-policy", policy === null
34
+ ? ancestors
35
+ : policy.replace(/frame-ancestors[^;]*/g, ancestors) +
36
+ (policy.includes("frame-ancestors") ? "" : `; ${ancestors}`));
37
+ }
38
+ return response;
39
+ };
40
+ }
41
+ /**
42
+ * Whether a form post came from the app's own pages. A decision must never be made from another
43
+ * site, which could otherwise approve an agent in the member's name. Fetch Metadata describes
44
+ * the browser's public request even when a trusted proxy rewrites the server's request URL.
45
+ */
46
+ export function isSameOrigin(request) {
47
+ const site = request.headers.get("sec-fetch-site");
48
+ if (site !== null) {
49
+ return site === "same-origin";
50
+ }
51
+ const supplied = request.headers.get("origin");
52
+ const origin = supplied === null ? undefined : httpOrigin(supplied);
53
+ if (origin === undefined) {
54
+ return false;
55
+ }
56
+ if (origin === new URL(request.url).origin) {
57
+ return true;
58
+ }
59
+ const host = (request.headers.get("x-forwarded-host") ?? request.headers.get("host"))
60
+ ?.split(",")[0]
61
+ ?.trim();
62
+ if (!host) {
63
+ return false;
64
+ }
65
+ const forwardedProto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
66
+ const protocol = forwardedProto === "http" || forwardedProto === "https"
67
+ ? forwardedProto
68
+ : isLoopbackHostname(host.replace(/:\d+$/, ""))
69
+ ? "http"
70
+ : "https";
71
+ return origin === httpOrigin(`${protocol}://${host}`);
72
+ }
73
+ /** An Origin names only an HTTP(S) authority, never credentials, a path or query. */
74
+ function httpOrigin(value) {
75
+ if (!URL.canParse(value)) {
76
+ return undefined;
77
+ }
78
+ const url = new URL(value);
79
+ return (url.protocol === "http:" || url.protocol === "https:") &&
80
+ url.username === "" &&
81
+ url.password === "" &&
82
+ url.pathname === "/" &&
83
+ url.search === "" &&
84
+ url.hash === ""
85
+ ? url.origin
86
+ : undefined;
87
+ }
@@ -0,0 +1,30 @@
1
+ /** Runtime observations; authorization of the machine credential remains platform-owned. */
2
+ export interface DeploymentCapabilities {
3
+ readonly warehouseRead: boolean;
4
+ readonly pipelineRuns: boolean;
5
+ readonly connections: boolean;
6
+ readonly agents: boolean;
7
+ readonly workflows: boolean;
8
+ }
9
+ export type DeploymentObservation = {
10
+ readonly target: "production";
11
+ readonly deploymentId?: string;
12
+ readonly capabilities: DeploymentCapabilities;
13
+ } | {
14
+ readonly target: "preview";
15
+ readonly deploymentId: string;
16
+ readonly expiresAt: string;
17
+ readonly capabilities: DeploymentCapabilities;
18
+ };
19
+ export type DeploymentAdmissionCode = "preview_configuration_invalid" | "preview_expired" | "preview_capability_unavailable" | "preview_authentication_unavailable" | "preview_storage_binding_invalid";
20
+ /** Stable codes only: workspace code owns explanations and presentation. */
21
+ export declare class DeploymentAdmissionError extends Error {
22
+ readonly code: DeploymentAdmissionCode;
23
+ readonly capability?: keyof DeploymentCapabilities | undefined;
24
+ readonly _tag = "DeploymentAdmissionError";
25
+ constructor(code: DeploymentAdmissionCode, capability?: keyof DeploymentCapabilities | undefined);
26
+ get status(): number;
27
+ }
28
+ export declare function deploymentFromEnv(environment?: Readonly<Record<string, string | undefined>>): DeploymentObservation;
29
+ /** Recheck the deadline on every admission, including a warm process and cached sessions. */
30
+ export declare function requireDeploymentCapability(deployment: DeploymentObservation, capability?: keyof DeploymentCapabilities, now?: number): void;
@@ -0,0 +1,72 @@
1
+ /** Stable codes only: workspace code owns explanations and presentation. */
2
+ export class DeploymentAdmissionError extends Error {
3
+ code;
4
+ capability;
5
+ _tag = "DeploymentAdmissionError";
6
+ constructor(code, capability) {
7
+ super(code);
8
+ this.code = code;
9
+ this.capability = capability;
10
+ this.name = "DeploymentAdmissionError";
11
+ }
12
+ get status() {
13
+ return this.code === "preview_expired"
14
+ ? 410
15
+ : this.code === "preview_configuration_invalid" ||
16
+ this.code === "preview_storage_binding_invalid"
17
+ ? 503
18
+ : 403;
19
+ }
20
+ }
21
+ export function deploymentFromEnv(environment = process.env) {
22
+ const target = environment.SEGMENTSTREAM_DEPLOYMENT_TARGET?.trim() || "production";
23
+ const deploymentId = environment.SEGMENTSTREAM_ONTOLOGY_DEPLOYMENT_ID?.trim();
24
+ if (target === "production") {
25
+ return Object.freeze({
26
+ target,
27
+ ...(deploymentId ? { deploymentId } : {}),
28
+ capabilities: Object.freeze({
29
+ warehouseRead: true,
30
+ pipelineRuns: true,
31
+ connections: true,
32
+ agents: true,
33
+ workflows: true,
34
+ }),
35
+ });
36
+ }
37
+ const expiresAt = environment.SEGMENTSTREAM_PREVIEW_EXPIRES_AT?.trim();
38
+ if (target !== "preview" ||
39
+ !deploymentId ||
40
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(deploymentId) ||
41
+ !expiresAt ||
42
+ !/^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(expiresAt) ||
43
+ !Number.isFinite(Date.parse(expiresAt))) {
44
+ throw new DeploymentAdmissionError("preview_configuration_invalid");
45
+ }
46
+ return Object.freeze({
47
+ target,
48
+ deploymentId,
49
+ expiresAt,
50
+ capabilities: Object.freeze({
51
+ warehouseRead: true,
52
+ pipelineRuns: false,
53
+ connections: false,
54
+ agents: false,
55
+ workflows: true,
56
+ }),
57
+ });
58
+ }
59
+ /** Recheck the deadline on every admission, including a warm process and cached sessions. */
60
+ export function requireDeploymentCapability(deployment, capability, now = Date.now()) {
61
+ if (deployment.target !== "preview")
62
+ return;
63
+ if (!Number.isFinite(now) ||
64
+ !Number.isFinite(Date.parse(deployment.expiresAt)) ||
65
+ now >= Date.parse(deployment.expiresAt)) {
66
+ throw new DeploymentAdmissionError("preview_expired");
67
+ }
68
+ // The preview profile is fixed; supplied flags cannot enable managed integrations.
69
+ if (capability !== undefined && capability !== "warehouseRead" && capability !== "workflows") {
70
+ throw new DeploymentAdmissionError("preview_capability_unavailable", capability);
71
+ }
72
+ }