@segmentstream/app 0.0.0-stage → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +2 -0
- package/README.md +1236 -2
- package/dist/agent.d.ts +56 -0
- package/dist/agent.js +203 -0
- package/dist/app.d.ts +82 -0
- package/dist/app.js +434 -0
- package/dist/browser-auth-capability.d.ts +2 -0
- package/dist/browser-auth-capability.js +2 -0
- package/dist/browser-revision.d.ts +19 -0
- package/dist/browser-revision.js +140 -0
- package/dist/channel-protocol.d.ts +16 -0
- package/dist/channel-protocol.js +17 -0
- package/dist/channel-session.d.ts +22 -0
- package/dist/channel-session.js +89 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.js +112 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.js +1 -0
- package/dist/cookies.d.ts +56 -0
- package/dist/cookies.js +131 -0
- package/dist/data.d.ts +233 -0
- package/dist/data.js +206 -0
- package/dist/decision-pages.d.ts +14 -0
- package/dist/decision-pages.js +87 -0
- package/dist/deployment.d.ts +30 -0
- package/dist/deployment.js +72 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.js +123 -0
- package/dist/federation.d.ts +24 -0
- package/dist/federation.js +613 -0
- package/dist/groups.d.ts +95 -0
- package/dist/groups.js +280 -0
- package/dist/handlers.d.ts +10 -0
- package/dist/handlers.js +161 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +29 -0
- package/dist/jobs.d.ts +49 -0
- package/dist/jobs.js +40 -0
- package/dist/legacy.d.ts +77 -0
- package/dist/legacy.js +57 -0
- package/dist/logo.d.ts +1 -0
- package/dist/logo.js +25 -0
- package/dist/me.d.ts +42 -0
- package/dist/me.js +54 -0
- package/dist/navigation/build.d.ts +68 -0
- package/dist/navigation/build.js +290 -0
- package/dist/navigation/cli.d.ts +19 -0
- package/dist/navigation/cli.js +37 -0
- package/dist/navigation/generate.d.ts +43 -0
- package/dist/navigation/generate.js +138 -0
- package/dist/navigation/next.d.ts +10 -0
- package/dist/navigation/next.js +18 -0
- package/dist/navigation-protocol.d.ts +55 -0
- package/dist/navigation-protocol.js +298 -0
- package/dist/neon.d.ts +101 -0
- package/dist/neon.js +539 -0
- package/dist/oauth-completion.d.ts +6 -0
- package/dist/oauth-completion.js +106 -0
- package/dist/oauth.d.ts +59 -0
- package/dist/oauth.js +129 -0
- package/dist/ontology-names.d.ts +10 -0
- package/dist/ontology-names.js +38 -0
- package/dist/ontology-transport.d.ts +34 -0
- package/dist/ontology-transport.js +130 -0
- package/dist/ontology-wire.d.ts +66 -0
- package/dist/ontology-wire.js +355 -0
- package/dist/ontology.d.ts +381 -0
- package/dist/ontology.js +367 -0
- package/dist/pages.d.ts +6 -0
- package/dist/pages.js +6 -0
- package/dist/parameters.d.ts +55 -0
- package/dist/parameters.js +118 -0
- package/dist/permissions.d.ts +19 -0
- package/dist/permissions.js +55 -0
- package/dist/pkce.d.ts +8 -0
- package/dist/pkce.js +40 -0
- package/dist/proxy.d.ts +25 -0
- package/dist/proxy.js +135 -0
- package/dist/request-timing.d.ts +6 -0
- package/dist/request-timing.js +53 -0
- package/dist/return-to.d.ts +2 -0
- package/dist/return-to.js +24 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +80 -0
- package/dist/sessions.d.ts +27 -0
- package/dist/sessions.js +72 -0
- package/dist/sign-in.d.ts +37 -0
- package/dist/sign-in.js +95 -0
- package/dist/storage.d.ts +79 -0
- package/dist/storage.js +180 -0
- package/dist/telegram-launch.d.ts +7 -0
- package/dist/telegram-launch.js +134 -0
- package/dist/telegram.d.ts +2 -0
- package/dist/telegram.js +6 -0
- package/dist/tokens.d.ts +51 -0
- package/dist/tokens.js +70 -0
- package/dist/ui/agent-connect.d.ts +18 -0
- package/dist/ui/agent-connect.js +23 -0
- package/dist/ui/components.d.ts +27 -0
- package/dist/ui/components.js +71 -0
- package/dist/ui/hooks.d.ts +19 -0
- package/dist/ui/hooks.js +56 -0
- package/dist/ui/index.d.ts +22 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/navigation.d.ts +5 -0
- package/dist/ui/navigation.js +8 -0
- package/dist/ui/segmentstream-logo.d.ts +2 -0
- package/dist/ui/segmentstream-logo.js +7 -0
- package/dist/ui/sign-in-check.d.ts +14 -0
- package/dist/ui/sign-in-check.js +41 -0
- package/dist/ui/sign-in.d.ts +17 -0
- package/dist/ui/sign-in.js +65 -0
- package/dist/ui/store.d.ts +80 -0
- package/dist/ui/store.js +280 -0
- package/dist/ui/styles.css +249 -0
- package/dist/ui/telegram-sign-in.d.ts +16 -0
- package/dist/ui/telegram-sign-in.js +21 -0
- package/dist/ui/use-browser-sign-in.d.ts +52 -0
- package/dist/ui/use-browser-sign-in.js +169 -0
- package/dist/ui/use-channel-sign-in.d.ts +10 -0
- package/dist/ui/use-channel-sign-in.js +118 -0
- package/dist/ui/use-sign-in.d.ts +109 -0
- package/dist/ui/use-sign-in.js +304 -0
- package/package.json +95 -4
package/dist/ontology.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { SegmentStreamApiError, apiErrorFrom, readJson } from "./errors.js";
|
|
2
|
+
import { metricQueryPoster, withEarlierQueryMetricsName, } from "./legacy.js";
|
|
3
|
+
import { actionInputToWire, encodeWhere, editsFromWire, includeToWire, queryToWire, recordFromWire, responseFromWire, returnsUserRecord, } from "./ontology-wire.js";
|
|
4
|
+
import { toCamelCase, toReversibleCamelCase, toSnakeCase } from "./ontology-names.js";
|
|
5
|
+
import { createBlobStorageClient } from "./storage.js";
|
|
6
|
+
const ENTITY_NAME = /^[a-z][a-zA-Z0-9]*(?:_[a-z0-9]+)*$/;
|
|
7
|
+
const ACTION_NAME = ENTITY_NAME;
|
|
8
|
+
/** @internal The ontology service's metric query route, model-level and per entity; never an entity. */
|
|
9
|
+
export const QUERY_METRICS_ROUTE = "query_metrics";
|
|
10
|
+
/** Names a caller may probe on any object; never an entity or an action. */
|
|
11
|
+
const RESERVED = new Set([
|
|
12
|
+
"then",
|
|
13
|
+
"catch",
|
|
14
|
+
"finally",
|
|
15
|
+
"constructor",
|
|
16
|
+
"toString",
|
|
17
|
+
"toLocaleString",
|
|
18
|
+
"valueOf",
|
|
19
|
+
"hasOwnProperty",
|
|
20
|
+
"isPrototypeOf",
|
|
21
|
+
"propertyIsEnumerable",
|
|
22
|
+
]);
|
|
23
|
+
// Node's inspection hook is Symbol.for("nodejs.util.inspect.custom"). Symbols
|
|
24
|
+
// are ignored by the proxies below; the string "inspect" is a valid domain name.
|
|
25
|
+
/**
|
|
26
|
+
* Entities and actions exist only in the app's types, so the client resolves them by name when
|
|
27
|
+
* they are used: `ontology.lead.actions.changeStatus(…)` posts to `lead/actions/change_status`.
|
|
28
|
+
*/
|
|
29
|
+
export function createOntologyClient(transport) {
|
|
30
|
+
const entities = new Map();
|
|
31
|
+
const catalogReader = createCatalogReader(transport);
|
|
32
|
+
// Every call, metric queries included, carries the user's session for the service to check.
|
|
33
|
+
const postMetricQuery = metricQueryPoster(QUERY_METRICS_ROUTE, (path, body, signal) => dispatch(catalogReader.transport, "POST", path, body, signal));
|
|
34
|
+
const queryMetrics = async (request, options = {}) => modelMetricQueryResultFromWire(await payloadOf(await postMetricQuery("", metricQueryToWire(request), options.signal)));
|
|
35
|
+
const model = {
|
|
36
|
+
queryMetrics,
|
|
37
|
+
...withEarlierQueryMetricsName(queryMetrics),
|
|
38
|
+
};
|
|
39
|
+
return new Proxy(Object.create(null), {
|
|
40
|
+
get(_target, property) {
|
|
41
|
+
if (typeof property === "string" && Object.hasOwn(model, property)) {
|
|
42
|
+
return model[property];
|
|
43
|
+
}
|
|
44
|
+
if (typeof property !== "string" || RESERVED.has(property) || !ENTITY_NAME.test(property)) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
let client = entities.get(property);
|
|
48
|
+
if (client === undefined) {
|
|
49
|
+
const entity = toSnakeCase(property);
|
|
50
|
+
// Names are relative to the entity and convert as record fields do.
|
|
51
|
+
const queryEntityMetrics = async (request, options = {}) => metricQueryResultFromWire(await payloadOf(await postMetricQuery(`/${entity}`, metricQueryToWire(request), options.signal)), toCamelCase);
|
|
52
|
+
client = {
|
|
53
|
+
...entityClient(entity, catalogReader.transport, catalogReader.getCatalog),
|
|
54
|
+
queryMetrics: queryEntityMetrics,
|
|
55
|
+
...withEarlierQueryMetricsName(queryEntityMetrics),
|
|
56
|
+
};
|
|
57
|
+
entities.set(property, client);
|
|
58
|
+
}
|
|
59
|
+
return client;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function createCatalogReader(transport) {
|
|
64
|
+
let catalogPromise;
|
|
65
|
+
let catalogHash;
|
|
66
|
+
const observedTransport = {
|
|
67
|
+
async fetch(path, init) {
|
|
68
|
+
const response = await transport.fetch(path, init);
|
|
69
|
+
const hash = response.headers.get("x-ontology-hash");
|
|
70
|
+
if (catalogHash !== undefined && hash !== null && hash !== catalogHash) {
|
|
71
|
+
catalogPromise = undefined;
|
|
72
|
+
catalogHash = undefined;
|
|
73
|
+
}
|
|
74
|
+
return response;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
async function readCatalog() {
|
|
78
|
+
try {
|
|
79
|
+
const catalog = await send(observedTransport, "GET", "", undefined, undefined);
|
|
80
|
+
catalogHash = catalog.hash;
|
|
81
|
+
return catalog;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
catalogPromise = undefined;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function getCatalog() {
|
|
89
|
+
catalogPromise ??= readCatalog();
|
|
90
|
+
return catalogPromise;
|
|
91
|
+
}
|
|
92
|
+
return { transport: observedTransport, getCatalog };
|
|
93
|
+
}
|
|
94
|
+
function entityClient(entity, transport, getCatalog) {
|
|
95
|
+
const base = `/${entity}`;
|
|
96
|
+
const storage = createBlobStorageClient(transport);
|
|
97
|
+
const actionCalls = new Map();
|
|
98
|
+
const runAction = async (name, input, options = {}) => {
|
|
99
|
+
const action = toSnakeCase(name);
|
|
100
|
+
const catalog = await getCatalog();
|
|
101
|
+
const wireResponse = await send(transport, "POST", `${base}/actions/${encodeURIComponent(action)}`, {
|
|
102
|
+
input: actionInputToWire(input, entity, action, catalog),
|
|
103
|
+
...([options.validateOnly, options.idempotencyKey, options.createIntent].every((value) => value === undefined)
|
|
104
|
+
? {}
|
|
105
|
+
: {
|
|
106
|
+
options: {
|
|
107
|
+
...(options.validateOnly === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { validate_only: options.validateOnly }),
|
|
110
|
+
...(options.idempotencyKey === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { idempotency_key: options.idempotencyKey }),
|
|
113
|
+
...(options.createIntent === undefined
|
|
114
|
+
? {}
|
|
115
|
+
: { create_intent: options.createIntent }),
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
}, options.signal);
|
|
119
|
+
const response = { ...wireResponse, edits: editsFromWire(wireResponse.edits, catalog) };
|
|
120
|
+
if (action === "create" || action === "update" || returnsUserRecord(entity, action)) {
|
|
121
|
+
return { ...response, result: recordFromWire(response.result, entity, catalog) };
|
|
122
|
+
}
|
|
123
|
+
if (action.startsWith("upsert_by_") &&
|
|
124
|
+
response.result !== null &&
|
|
125
|
+
typeof response.result === "object" &&
|
|
126
|
+
!Array.isArray(response.result)) {
|
|
127
|
+
return {
|
|
128
|
+
...response,
|
|
129
|
+
result: {
|
|
130
|
+
...response.result,
|
|
131
|
+
record: recordFromWire(response.result.record, entity, catalog),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return response;
|
|
136
|
+
};
|
|
137
|
+
const actions = new Proxy(Object.create(null), {
|
|
138
|
+
get(_target, property) {
|
|
139
|
+
if (typeof property !== "string" || RESERVED.has(property) || !ACTION_NAME.test(property)) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
let call = actionCalls.get(property);
|
|
143
|
+
if (call === undefined) {
|
|
144
|
+
call = (input, options) => runAction(property, input, options);
|
|
145
|
+
actionCalls.set(property, call);
|
|
146
|
+
}
|
|
147
|
+
return call;
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
files: {
|
|
152
|
+
download: (id, property, options) => storage.download(entity, id, property, options),
|
|
153
|
+
},
|
|
154
|
+
find: async (query = {}, options = {}) => {
|
|
155
|
+
const catalog = await getCatalog();
|
|
156
|
+
const where = await encodeWhere({ where: query.where, entity, getCatalog });
|
|
157
|
+
const search = findParameters({ ...query, where }).toString();
|
|
158
|
+
const page = await send(transport, "GET", search === "" ? base : `${base}?${search}`, undefined, options.signal);
|
|
159
|
+
validateProjection(page, query.select);
|
|
160
|
+
return {
|
|
161
|
+
...page,
|
|
162
|
+
items: page.items.map((record) => recordFromWire(record, entity, catalog)),
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
query: async (query = {}, options = {}) => {
|
|
166
|
+
const catalog = await getCatalog();
|
|
167
|
+
const where = await encodeWhere({ where: query.where, entity, getCatalog });
|
|
168
|
+
const page = await send(transport, "POST", `${base}/query`, queryToWire({ ...query, where }), options.signal);
|
|
169
|
+
validateProjection(page, query.select);
|
|
170
|
+
return {
|
|
171
|
+
...page,
|
|
172
|
+
items: page.items.map((record) => recordFromWire(record, entity, catalog)),
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
get: (id, options = {}) => {
|
|
176
|
+
const encodedId = recordId(id);
|
|
177
|
+
return getCatalog().then(async (catalog) => recordFromWire(await send(transport, "GET", `${base}/${encodedId}`, undefined, options.signal), entity, catalog));
|
|
178
|
+
},
|
|
179
|
+
history: (id, options = {}) => {
|
|
180
|
+
const search = new URLSearchParams();
|
|
181
|
+
if (options.after !== undefined) {
|
|
182
|
+
search.set("after", options.after);
|
|
183
|
+
}
|
|
184
|
+
if (options.limit !== undefined) {
|
|
185
|
+
search.set("limit", String(options.limit));
|
|
186
|
+
}
|
|
187
|
+
const encoded = search.toString();
|
|
188
|
+
const suffix = encoded === "" ? "" : `?${encoded}`;
|
|
189
|
+
return send(transport, "GET", `${base}/${recordId(id)}/history${suffix}`, undefined, options.signal);
|
|
190
|
+
},
|
|
191
|
+
actions,
|
|
192
|
+
action: (name, input, options) => {
|
|
193
|
+
if (!ACTION_NAME.test(name)) {
|
|
194
|
+
throw new TypeError(`"${name}" is not an action name.`);
|
|
195
|
+
}
|
|
196
|
+
return runAction(name, input, options);
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** A projected response must never masquerade as a complete typed record (or vice versa). */
|
|
201
|
+
function validateProjection(page, selection) {
|
|
202
|
+
const invalid = selection === undefined
|
|
203
|
+
? page.projected !== undefined
|
|
204
|
+
: page.projected !== true ||
|
|
205
|
+
page.items.some((record) => typeof record.id !== "string" ||
|
|
206
|
+
selection.some((field) => !Object.hasOwn(record, toSnakeCase(field))));
|
|
207
|
+
if (invalid) {
|
|
208
|
+
throw new SegmentStreamApiError({
|
|
209
|
+
code: "ontology_invalid_response",
|
|
210
|
+
status: 502,
|
|
211
|
+
detail: "The ontology service returned an incompatible record projection.",
|
|
212
|
+
retryable: false,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function recordId(id) {
|
|
217
|
+
if (typeof id !== "string" || id === "") {
|
|
218
|
+
throw new TypeError("A record id is a non-empty string.");
|
|
219
|
+
}
|
|
220
|
+
return encodeURIComponent(id);
|
|
221
|
+
}
|
|
222
|
+
/** `find` sends the query as URL parameters: `where` as JSON, `orderBy` as `field:direction`. */
|
|
223
|
+
export function findParameters(query) {
|
|
224
|
+
const search = new URLSearchParams();
|
|
225
|
+
if (query.select !== undefined) {
|
|
226
|
+
search.set("select", query.select.map(toSnakeCase).join(","));
|
|
227
|
+
}
|
|
228
|
+
if (query.where !== undefined) {
|
|
229
|
+
search.set("where", JSON.stringify(query.where));
|
|
230
|
+
}
|
|
231
|
+
if (query.orderBy !== undefined && query.orderBy.length > 0) {
|
|
232
|
+
search.set("order_by", query.orderBy
|
|
233
|
+
.map(({ field, direction }) => `${toSnakeCase(field)}:${direction ?? "asc"}`)
|
|
234
|
+
.join(","));
|
|
235
|
+
}
|
|
236
|
+
if (query.include !== undefined) {
|
|
237
|
+
const relationships = Object.entries(includeToWire(query.include))
|
|
238
|
+
.filter(([, included]) => included === true)
|
|
239
|
+
.map(([name]) => name);
|
|
240
|
+
if (relationships.length > 0) {
|
|
241
|
+
search.set("include", relationships.join(","));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (query.after !== undefined) {
|
|
245
|
+
search.set("after", query.after);
|
|
246
|
+
}
|
|
247
|
+
if (query.limit !== undefined) {
|
|
248
|
+
search.set("limit", String(query.limit));
|
|
249
|
+
}
|
|
250
|
+
return search;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Metric queries keep their camelCase protocol keys and filter values; the names in them convert
|
|
254
|
+
* to snake_case one segment at a time.
|
|
255
|
+
*/
|
|
256
|
+
function metricQueryToWire(request) {
|
|
257
|
+
return {
|
|
258
|
+
...request,
|
|
259
|
+
...(request.dimensions === undefined
|
|
260
|
+
? {}
|
|
261
|
+
: { dimensions: request.dimensions.map(toSnakeCase) }),
|
|
262
|
+
metrics: request.metrics.map(toSnakeCase),
|
|
263
|
+
...(request.granularityOf === undefined
|
|
264
|
+
? {}
|
|
265
|
+
: { granularityOf: toSnakeCase(request.granularityOf) }),
|
|
266
|
+
...(request.filters === undefined
|
|
267
|
+
? {}
|
|
268
|
+
: {
|
|
269
|
+
filters: request.filters.map((filter) => ({
|
|
270
|
+
...filter,
|
|
271
|
+
dimension: toSnakeCase(filter.dimension),
|
|
272
|
+
})),
|
|
273
|
+
}),
|
|
274
|
+
...(request.orderBy === undefined
|
|
275
|
+
? {}
|
|
276
|
+
: {
|
|
277
|
+
orderBy: request.orderBy.map((order) => ({
|
|
278
|
+
...order,
|
|
279
|
+
...(order.dimension === undefined ? {} : { dimension: toSnakeCase(order.dimension) }),
|
|
280
|
+
...(order.metric === undefined ? {} : { metric: toSnakeCase(order.metric) }),
|
|
281
|
+
})),
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Field names, the filters each metric applied, a balance's dimension, a model metric's inputs and
|
|
287
|
+
* row keys come back camelCase per segment; `execution` is passed on as sent.
|
|
288
|
+
*/
|
|
289
|
+
function metricQueryResultFromWire(result, decodeName) {
|
|
290
|
+
return {
|
|
291
|
+
...result,
|
|
292
|
+
fields: result.fields.map((field) => ({
|
|
293
|
+
...field,
|
|
294
|
+
name: decodeName(field.name),
|
|
295
|
+
...(field.filters === undefined ? {} : { filters: field.filters.map(decodeName) }),
|
|
296
|
+
...(field.balanceOver === undefined ? {} : { balanceOver: decodeName(field.balanceOver) }),
|
|
297
|
+
...(field.inputs === undefined ? {} : { inputs: field.inputs.map(decodeName) }),
|
|
298
|
+
})),
|
|
299
|
+
rows: result.rows.map((row) => Object.fromEntries(Object.entries(row).map(([name, value]) => [decodeName(name), value]))),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Model-level names keep an `_` before a digit; the entities in `sources` and the dimensions in
|
|
304
|
+
* `periods` convert too.
|
|
305
|
+
*/
|
|
306
|
+
function modelMetricQueryResultFromWire(wire) {
|
|
307
|
+
const result = metricQueryResultFromWire(wire, toReversibleCamelCase);
|
|
308
|
+
const { sources, periods } = result.execution;
|
|
309
|
+
if (sources === undefined && periods === undefined) {
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
...result,
|
|
314
|
+
execution: {
|
|
315
|
+
...result.execution,
|
|
316
|
+
...(sources === undefined
|
|
317
|
+
? {}
|
|
318
|
+
: {
|
|
319
|
+
sources: sources.map((source) => ({
|
|
320
|
+
...source,
|
|
321
|
+
entity: toReversibleCamelCase(source.entity),
|
|
322
|
+
})),
|
|
323
|
+
}),
|
|
324
|
+
...(periods === undefined
|
|
325
|
+
? {}
|
|
326
|
+
: {
|
|
327
|
+
periods: periods.map((period) => ({
|
|
328
|
+
...period,
|
|
329
|
+
dimension: toReversibleCamelCase(period.dimension),
|
|
330
|
+
})),
|
|
331
|
+
}),
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
async function send(transport, method, path, body, signal) {
|
|
336
|
+
const payload = await payloadOf(await dispatch(transport, method, path, body, signal));
|
|
337
|
+
return responseFromWire({ method, path, payload });
|
|
338
|
+
}
|
|
339
|
+
/** Sends one request to the ontology service, its body as JSON. */
|
|
340
|
+
function dispatch(transport, method, path, body, signal) {
|
|
341
|
+
const headers = new Headers({ accept: "application/json" });
|
|
342
|
+
if (body !== undefined) {
|
|
343
|
+
headers.set("content-type", "application/json");
|
|
344
|
+
}
|
|
345
|
+
return transport.fetch(path, {
|
|
346
|
+
method,
|
|
347
|
+
headers,
|
|
348
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
349
|
+
...(signal === undefined ? {} : { signal }),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/** The service's JSON answer, or the error it answered with. */
|
|
353
|
+
async function payloadOf(response) {
|
|
354
|
+
const payload = await readJson(response);
|
|
355
|
+
if (!response.ok) {
|
|
356
|
+
throw apiErrorFrom(response.status, payload, "The ontology service rejected the request.");
|
|
357
|
+
}
|
|
358
|
+
if (payload === null) {
|
|
359
|
+
throw new SegmentStreamApiError({
|
|
360
|
+
code: "ontology_invalid_response",
|
|
361
|
+
status: 502,
|
|
362
|
+
detail: "The ontology service answered without a JSON body.",
|
|
363
|
+
retryable: true,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return payload;
|
|
367
|
+
}
|
package/dist/pages.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The app's page where a signed-in member decides whether an agent may act for them. */
|
|
2
|
+
export declare const CONSENT_PAGE = "/oauth/consent";
|
|
3
|
+
/** The app's page where a signed-in member links their chat account to the workspace agent. */
|
|
4
|
+
export declare const AGENT_CONNECT_PAGE = "/agent/connect";
|
|
5
|
+
/** The Route Handler the agent connect page's form posts to. */
|
|
6
|
+
export declare const AGENT_CONNECT_HANDLER = "/api/agent/connect";
|
package/dist/pages.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The app's page where a signed-in member decides whether an agent may act for them. */
|
|
2
|
+
export const CONSENT_PAGE = "/oauth/consent";
|
|
3
|
+
/** The app's page where a signed-in member links their chat account to the workspace agent. */
|
|
4
|
+
export const AGENT_CONNECT_PAGE = "/agent/connect";
|
|
5
|
+
/** The Route Handler the agent connect page's form posts to. */
|
|
6
|
+
export const AGENT_CONNECT_HANDLER = "/api/agent/connect";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export declare const PARAMETER_TYPES: readonly ["STRING", "INT64", "FLOAT64", "NUMERIC", "BIGNUMERIC", "BOOL", "DATE", "DATETIME", "TIMESTAMP"];
|
|
2
|
+
export type ParameterType = (typeof PARAMETER_TYPES)[number];
|
|
3
|
+
declare const TYPED: unique symbol;
|
|
4
|
+
/** A value with an explicit warehouse type, created with the `sql` helpers. */
|
|
5
|
+
export interface TypedParameter {
|
|
6
|
+
readonly [TYPED]: true;
|
|
7
|
+
readonly type: ParameterType;
|
|
8
|
+
readonly value?: string;
|
|
9
|
+
readonly values?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
/** A value bound to a named `@parameter`; never spliced into the SQL text. */
|
|
12
|
+
export type QueryParameterValue = string | number | bigint | boolean | Date | TypedParameter | ReadonlyArray<string | number | bigint | boolean | Date>;
|
|
13
|
+
export type QueryParameters = Readonly<Record<string, QueryParameterValue>>;
|
|
14
|
+
/** The wire form the SegmentStream API accepts. */
|
|
15
|
+
export type EncodedParameter = {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly type: ParameterType;
|
|
18
|
+
readonly value: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly type: ParameterType;
|
|
22
|
+
readonly values: readonly string[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Explicit parameter types for values JavaScript cannot express unambiguously.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* await segmentstream.warehouse.query(
|
|
29
|
+
* "SELECT * FROM gold.orders WHERE order_date >= @from AND amount > @minimum",
|
|
30
|
+
* { params: { from: sql.date("2026-09-01"), minimum: sql.numeric("10.50") } },
|
|
31
|
+
* );
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare const sql: {
|
|
35
|
+
readonly string: (value: string) => TypedParameter;
|
|
36
|
+
readonly int64: (value: number | bigint | string) => TypedParameter;
|
|
37
|
+
readonly float64: (value: number) => TypedParameter;
|
|
38
|
+
readonly numeric: (value: string | number) => TypedParameter;
|
|
39
|
+
readonly bignumeric: (value: string | number) => TypedParameter;
|
|
40
|
+
readonly bool: (value: boolean) => TypedParameter;
|
|
41
|
+
/** A calendar date, `YYYY-MM-DD`; a `Date` uses its UTC day. */
|
|
42
|
+
readonly date: (value: string | Date) => TypedParameter;
|
|
43
|
+
/** A civil date and time without a time zone, `YYYY-MM-DDTHH:MM:SS[.ffffff]`. */
|
|
44
|
+
readonly datetime: (value: string) => TypedParameter;
|
|
45
|
+
readonly timestamp: (value: string | Date) => TypedParameter;
|
|
46
|
+
/** An array of one element type; needed for empty arrays and non-default element types. */
|
|
47
|
+
readonly array: (type: ParameterType, values: ReadonlyArray<string | number | bigint | boolean | Date>) => {
|
|
48
|
+
[TYPED]: true;
|
|
49
|
+
type: "STRING" | "INT64" | "FLOAT64" | "NUMERIC" | "BIGNUMERIC" | "BOOL" | "DATE" | "DATETIME" | "TIMESTAMP";
|
|
50
|
+
values: string[];
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
/** Infers warehouse types for named parameters and validates their names. */
|
|
54
|
+
export declare function encodeParameters(parameters: QueryParameters | undefined): EncodedParameter[];
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export const PARAMETER_TYPES = [
|
|
2
|
+
"STRING",
|
|
3
|
+
"INT64",
|
|
4
|
+
"FLOAT64",
|
|
5
|
+
"NUMERIC",
|
|
6
|
+
"BIGNUMERIC",
|
|
7
|
+
"BOOL",
|
|
8
|
+
"DATE",
|
|
9
|
+
"DATETIME",
|
|
10
|
+
"TIMESTAMP",
|
|
11
|
+
];
|
|
12
|
+
const TYPED = Symbol.for("segmentstream.app.queryParameter");
|
|
13
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
14
|
+
function typed(type, value) {
|
|
15
|
+
return { [TYPED]: true, type, value };
|
|
16
|
+
}
|
|
17
|
+
function isoDate(value, name) {
|
|
18
|
+
if (value instanceof Date) {
|
|
19
|
+
if (Number.isNaN(value.getTime())) {
|
|
20
|
+
throw new TypeError(`sql.${name}() received an invalid Date.`);
|
|
21
|
+
}
|
|
22
|
+
return value.toISOString().slice(0, 10);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function timestamp(value) {
|
|
27
|
+
if (value instanceof Date) {
|
|
28
|
+
if (Number.isNaN(value.getTime())) {
|
|
29
|
+
throw new TypeError("sql.timestamp() received an invalid Date.");
|
|
30
|
+
}
|
|
31
|
+
return value.toISOString();
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Explicit parameter types for values JavaScript cannot express unambiguously.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* await segmentstream.warehouse.query(
|
|
40
|
+
* "SELECT * FROM gold.orders WHERE order_date >= @from AND amount > @minimum",
|
|
41
|
+
* { params: { from: sql.date("2026-09-01"), minimum: sql.numeric("10.50") } },
|
|
42
|
+
* );
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const sql = {
|
|
46
|
+
string: (value) => typed("STRING", value),
|
|
47
|
+
int64: (value) => typed("INT64", String(value)),
|
|
48
|
+
float64: (value) => typed("FLOAT64", String(value)),
|
|
49
|
+
numeric: (value) => typed("NUMERIC", String(value)),
|
|
50
|
+
bignumeric: (value) => typed("BIGNUMERIC", String(value)),
|
|
51
|
+
bool: (value) => typed("BOOL", String(value)),
|
|
52
|
+
/** A calendar date, `YYYY-MM-DD`; a `Date` uses its UTC day. */
|
|
53
|
+
date: (value) => typed("DATE", isoDate(value, "date")),
|
|
54
|
+
/** A civil date and time without a time zone, `YYYY-MM-DDTHH:MM:SS[.ffffff]`. */
|
|
55
|
+
datetime: (value) => typed("DATETIME", value),
|
|
56
|
+
timestamp: (value) => typed("TIMESTAMP", timestamp(value)),
|
|
57
|
+
/** An array of one element type; needed for empty arrays and non-default element types. */
|
|
58
|
+
array: (type, values) => ({
|
|
59
|
+
[TYPED]: true,
|
|
60
|
+
type,
|
|
61
|
+
values: values.map((value) => scalar(value, "array element").value),
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
function isTyped(value) {
|
|
65
|
+
return typeof value === "object" && value !== null && TYPED in value;
|
|
66
|
+
}
|
|
67
|
+
function scalar(value, name) {
|
|
68
|
+
switch (typeof value) {
|
|
69
|
+
case "string":
|
|
70
|
+
return { type: "STRING", value };
|
|
71
|
+
case "boolean":
|
|
72
|
+
return { type: "BOOL", value: String(value) };
|
|
73
|
+
case "bigint":
|
|
74
|
+
return { type: "INT64", value: String(value) };
|
|
75
|
+
case "number":
|
|
76
|
+
return Number.isSafeInteger(value)
|
|
77
|
+
? { type: "INT64", value: String(value) }
|
|
78
|
+
: { type: "FLOAT64", value: String(value) };
|
|
79
|
+
default:
|
|
80
|
+
if (value instanceof Date) {
|
|
81
|
+
return { type: "TIMESTAMP", value: timestamp(value) };
|
|
82
|
+
}
|
|
83
|
+
throw new TypeError(`Query parameter "${name}" must be a string, number, bigint, boolean, Date, or sql.* value; ` +
|
|
84
|
+
"use SQL NULL in the query text instead of a null parameter.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Infers warehouse types for named parameters and validates their names. */
|
|
88
|
+
export function encodeParameters(parameters) {
|
|
89
|
+
if (parameters === undefined) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
return Object.entries(parameters).map(([name, value]) => {
|
|
93
|
+
if (!NAME.test(name)) {
|
|
94
|
+
throw new TypeError(`Query parameter name "${name}" must start with a letter or underscore and contain only letters, digits, and underscores.`);
|
|
95
|
+
}
|
|
96
|
+
if (isTyped(value)) {
|
|
97
|
+
return value.values === undefined
|
|
98
|
+
? { name, type: value.type, value: value.value ?? "" }
|
|
99
|
+
: { name, type: value.type, values: value.values };
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
if (value.length === 0) {
|
|
103
|
+
throw new TypeError(`Query parameter "${name}" is an empty array; use sql.array(type, []) to declare its element type.`);
|
|
104
|
+
}
|
|
105
|
+
const items = value.map((item) => scalar(item, name));
|
|
106
|
+
const type = items.every((item) => item.type === items[0]?.type)
|
|
107
|
+
? items[0].type
|
|
108
|
+
: items.every((item) => item.type === "INT64" || item.type === "FLOAT64")
|
|
109
|
+
? "FLOAT64"
|
|
110
|
+
: undefined;
|
|
111
|
+
if (type === undefined) {
|
|
112
|
+
throw new TypeError(`Query parameter "${name}" mixes element types; use sql.array(type, values).`);
|
|
113
|
+
}
|
|
114
|
+
return { name, type, values: items.map((item) => item.value) };
|
|
115
|
+
}
|
|
116
|
+
return { name, ...scalar(value, name) };
|
|
117
|
+
});
|
|
118
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissions are `<entity>:<verb>` strings, such as `lead:changeStatus`. A role may use `*` for the
|
|
3
|
+
* entity (`*:read`), the verb (`lead:*`) or both (`*:*`); what an action or a read requires never
|
|
4
|
+
* holds a wildcard. A wildcard entity does not cover the built-in `agent_chat`: only a permission
|
|
5
|
+
* that names it does. Of the built-in `user` it covers only `read` and `update`, so member management
|
|
6
|
+
* (`user:invite`, `user:grant_roles`, …) and authored actions on `user` need `user:<verb>` or `user:*`.
|
|
7
|
+
*
|
|
8
|
+
* The matching rule is the ontology service's, copied from `@segmentstream/ontology`
|
|
9
|
+
* (`src/permissions.ts`) so that the app SDK and its browser components do not depend on the
|
|
10
|
+
* ontology runtime. Keep the two in step.
|
|
11
|
+
*/
|
|
12
|
+
/** Whether a granted permission, which may hold wildcards, covers the required one. */
|
|
13
|
+
export declare function grants(granted: string, required: string): boolean;
|
|
14
|
+
/** Anything that holds permissions, such as the signed-in user. */
|
|
15
|
+
export interface PermissionHolder {
|
|
16
|
+
readonly permissions: ReadonlyArray<string>;
|
|
17
|
+
}
|
|
18
|
+
/** Whether one of the holder's permissions equals `required` or covers it through a wildcard. */
|
|
19
|
+
export declare function isAllowed(required: string, holder: PermissionHolder): boolean;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissions are `<entity>:<verb>` strings, such as `lead:changeStatus`. A role may use `*` for the
|
|
3
|
+
* entity (`*:read`), the verb (`lead:*`) or both (`*:*`); what an action or a read requires never
|
|
4
|
+
* holds a wildcard. A wildcard entity does not cover the built-in `agent_chat`: only a permission
|
|
5
|
+
* that names it does. Of the built-in `user` it covers only `read` and `update`, so member management
|
|
6
|
+
* (`user:invite`, `user:grant_roles`, …) and authored actions on `user` need `user:<verb>` or `user:*`.
|
|
7
|
+
*
|
|
8
|
+
* The matching rule is the ontology service's, copied from `@segmentstream/ontology`
|
|
9
|
+
* (`src/permissions.ts`) so that the app SDK and its browser components do not depend on the
|
|
10
|
+
* ontology runtime. Keep the two in step.
|
|
11
|
+
*/
|
|
12
|
+
import { toSnakeCase } from "./ontology-names.js";
|
|
13
|
+
const WILDCARD = "*";
|
|
14
|
+
/** Entities only a permission that names them covers; `*:read` and `*:*` grant nothing on them. */
|
|
15
|
+
const EXPLICIT_GRANT_ENTITIES = new Set(["agent_chat"]);
|
|
16
|
+
/** Entities of which a wildcard entity covers only the listed verbs. */
|
|
17
|
+
const WILDCARD_ENTITY_VERBS = new Map([
|
|
18
|
+
["user", new Set(["read", "update"])],
|
|
19
|
+
]);
|
|
20
|
+
function wildcardEntityCovers(entity, verb) {
|
|
21
|
+
if (EXPLICIT_GRANT_ENTITIES.has(entity)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return WILDCARD_ENTITY_VERBS.get(entity)?.has(verb) ?? true;
|
|
25
|
+
}
|
|
26
|
+
/** `<entity>:<verb>` with lowercase snake_case entities and lowercase snake_case verbs, or `*` for either. */
|
|
27
|
+
const PERMISSION = /^(\*|[a-z][a-zA-Z0-9]*(?:_[a-z0-9]+)*):(\*|[a-z][a-zA-Z0-9]*(?:_[a-z0-9]+)*)$/;
|
|
28
|
+
function parsePermission(value) {
|
|
29
|
+
if (typeof value !== "string") {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const match = PERMISSION.exec(value);
|
|
33
|
+
return match === null
|
|
34
|
+
? undefined
|
|
35
|
+
: { entity: toSnakeCase(match[1]), verb: toSnakeCase(match[2]) };
|
|
36
|
+
}
|
|
37
|
+
/** Whether a granted permission, which may hold wildcards, covers the required one. */
|
|
38
|
+
export function grants(granted, required) {
|
|
39
|
+
const held = parsePermission(granted);
|
|
40
|
+
const wanted = parsePermission(required);
|
|
41
|
+
if (held === undefined || wanted === undefined) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return ((held.entity === WILDCARD
|
|
45
|
+
? wildcardEntityCovers(wanted.entity, wanted.verb)
|
|
46
|
+
: held.entity === wanted.entity) &&
|
|
47
|
+
(held.verb === WILDCARD || held.verb === wanted.verb));
|
|
48
|
+
}
|
|
49
|
+
/** Whether one of the holder's permissions equals `required` or covers it through a wildcard. */
|
|
50
|
+
export function isAllowed(required, holder) {
|
|
51
|
+
// Keep the ontology matcher semantics, including invalid-input rejection,
|
|
52
|
+
// while avoiding repeated parsing of preceding grants for exact matches.
|
|
53
|
+
return ((holder.permissions.includes(required) && grants(required, required)) ||
|
|
54
|
+
holder.permissions.some((granted) => grants(granted, required)));
|
|
55
|
+
}
|
package/dist/pkce.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function base64UrlEncode(bytes: Uint8Array): string;
|
|
2
|
+
export declare function base64UrlDecode(value: string): Uint8Array;
|
|
3
|
+
/** A URL-safe random value carrying `byteLength` bytes of entropy. */
|
|
4
|
+
export declare function randomToken(byteLength?: number): string;
|
|
5
|
+
/** The RFC 7636 S256 code challenge for a code verifier. */
|
|
6
|
+
export declare function createCodeChallenge(codeVerifier: string): Promise<string>;
|
|
7
|
+
/** Compares two strings without short-circuiting on the first differing character. */
|
|
8
|
+
export declare function constantTimeEqual(left: string, right: string): boolean;
|