@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
@@ -0,0 +1,59 @@
1
+ import type { OntologyTransport } from "./ontology.js";
2
+ import { CONSENT_PAGE } from "./pages.js";
3
+ export { CONSENT_PAGE };
4
+ /** Everything the member may do, or only the reads of it. */
5
+ export type AgentScope = "ontology" | "ontology:read";
6
+ /** An agent asking to act for the signed-in member, as the consent page shows it. */
7
+ export interface AgentAuthorizationRequest {
8
+ readonly id: string;
9
+ readonly client: {
10
+ /** The name the agent registered with. Agents choose it themselves, so show the host too. */
11
+ readonly name: string;
12
+ readonly uri: string | null;
13
+ /** Where the member's browser goes after the decision: the agent's own address. */
14
+ readonly redirectUri: string;
15
+ readonly redirectHost: string;
16
+ };
17
+ readonly scope: AgentScope;
18
+ /** What the member may do, and so the most the agent may do for them. */
19
+ readonly permissions: ReadonlyArray<string>;
20
+ readonly expiresAt: string;
21
+ }
22
+ /**
23
+ * Why a device code cannot be decided: it `expired`, was already `decided`, or is `unknown` (never
24
+ * issued, mistyped, cleaned up, or reported by an ontology service too old to say).
25
+ */
26
+ export type DeviceCodeUnavailable = "expired" | "decided" | "unknown";
27
+ /** A device code looked up for the signed-in member: its request, or why there is none. */
28
+ export type DeviceLookup = {
29
+ readonly status: "pending";
30
+ readonly request: AgentAuthorizationRequest;
31
+ } | {
32
+ readonly status: DeviceCodeUnavailable;
33
+ };
34
+ export interface AgentAuthorizations {
35
+ /**
36
+ * The request the consent page at `/oauth/consent?request=<id>` shows, for the signed-in member;
37
+ * `null` when it expired or was already decided. Server-only.
38
+ */
39
+ readonly request: (id: string) => Promise<AgentAuthorizationRequest | null>;
40
+ /**
41
+ * The request for a device code the member entered at `/oauth/consent?device=1`, or why there is
42
+ * none, so the consent page can tell an expired code from an already decided or unknown one.
43
+ * Server-only. Each lookup counts toward the member's verification attempts.
44
+ */
45
+ readonly deviceLookup: (userCode: string) => Promise<DeviceLookup>;
46
+ /** {@link AgentAuthorizations.deviceLookup}'s request, or `null` when there is none. */
47
+ readonly deviceRequest: (userCode: string) => Promise<AgentAuthorizationRequest | null>;
48
+ /**
49
+ * The Route Handler the consent page's form posts to, at `app/src/app/api/oauth/consent/route.ts`.
50
+ * It takes `request`, `decision` (`approve` or `deny`) and, to allow reading only, `scope`
51
+ * (`ontology:read`), and sends the browser back to the agent. `decision=lookup` with `user_code`
52
+ * redirects to the consent page for that device code.
53
+ */
54
+ readonly consentHandler: () => {
55
+ readonly POST: (request: Request) => Promise<Response>;
56
+ };
57
+ }
58
+ /** The consent flow, over the ontology service as the signed-in member. */
59
+ export declare function createAgentAuthorizations(transport: OntologyTransport): AgentAuthorizations;
package/dist/oauth.js ADDED
@@ -0,0 +1,129 @@
1
+ import { NextResponse } from "next/server";
2
+ import { isSameOrigin } from "./decision-pages.js";
3
+ import { apiErrorFrom, readJson } from "./errors.js";
4
+ import { loopbackRedirect, oauthCompletion } from "./oauth-completion.js";
5
+ import { CONSENT_PAGE } from "./pages.js";
6
+ export { CONSENT_PAGE };
7
+ function parseRequest(payload) {
8
+ if (typeof payload !== "object" || payload === null) {
9
+ return null;
10
+ }
11
+ const body = payload;
12
+ const client = (body.client ?? {});
13
+ if (typeof body.id !== "string" ||
14
+ typeof client.name !== "string" ||
15
+ typeof client.redirect_uri !== "string" ||
16
+ typeof client.redirect_host !== "string" ||
17
+ (body.scope !== "ontology" && body.scope !== "ontology:read") ||
18
+ !Array.isArray(body.permissions) ||
19
+ typeof body.expires_at !== "string") {
20
+ return null;
21
+ }
22
+ return {
23
+ id: body.id,
24
+ client: {
25
+ name: client.name,
26
+ uri: typeof client.uri === "string" ? client.uri : null,
27
+ redirectUri: client.redirect_uri,
28
+ redirectHost: client.redirect_host,
29
+ },
30
+ scope: body.scope,
31
+ permissions: body.permissions.filter((permission) => typeof permission === "string"),
32
+ expiresAt: body.expires_at,
33
+ };
34
+ }
35
+ function unavailableReason(payload) {
36
+ const reason = payload?.reason;
37
+ return reason === "expired" || reason === "decided" ? reason : "unknown";
38
+ }
39
+ function requestPath(id, decision) {
40
+ const path = `/oauth/requests/${encodeURIComponent(id)}`;
41
+ return decision === undefined ? path : `${path}/${decision}`;
42
+ }
43
+ /** The consent flow, over the ontology service as the signed-in member. */
44
+ export function createAgentAuthorizations(transport) {
45
+ async function deviceLookup(userCode) {
46
+ const response = await transport.fetch("/oauth/device/lookup", {
47
+ method: "POST",
48
+ headers: { "content-type": "application/json" },
49
+ body: JSON.stringify({ user_code: userCode }),
50
+ });
51
+ const payload = await readJson(response);
52
+ if (response.status === 404) {
53
+ return { status: unavailableReason(payload) };
54
+ }
55
+ if (!response.ok) {
56
+ throw apiErrorFrom(response.status, payload, "The verification attempt was refused.");
57
+ }
58
+ const request = parseRequest(payload);
59
+ return request === null ? { status: "unknown" } : { status: "pending", request };
60
+ }
61
+ return {
62
+ async request(id) {
63
+ if (id === "") {
64
+ return null;
65
+ }
66
+ const response = await transport.fetch(requestPath(id), {
67
+ method: "GET",
68
+ headers: { accept: "application/json" },
69
+ });
70
+ const payload = await readJson(response);
71
+ if (response.status === 404) {
72
+ return null;
73
+ }
74
+ if (!response.ok) {
75
+ throw apiErrorFrom(response.status, payload, "The ontology service refused the request.");
76
+ }
77
+ return parseRequest(payload);
78
+ },
79
+ deviceLookup,
80
+ async deviceRequest(userCode) {
81
+ const lookup = await deviceLookup(userCode);
82
+ return lookup.status === "pending" ? lookup.request : null;
83
+ },
84
+ consentHandler: () => ({
85
+ async POST(request) {
86
+ if (!isSameOrigin(request)) {
87
+ return NextResponse.json({ error: "forbidden", detail: "Decide on an agent from the app's own page." }, { status: 403 });
88
+ }
89
+ const form = await request.formData();
90
+ const id = String(form.get("request") ?? "");
91
+ if (form.get("decision") === "lookup") {
92
+ const url = new URL(CONSENT_PAGE, request.url);
93
+ url.searchParams.set("device", "1");
94
+ url.searchParams.set("user_code", String(form.get("user_code") ?? "").slice(0, 32));
95
+ return NextResponse.redirect(url, 303);
96
+ }
97
+ const device = id.startsWith("device:");
98
+ const decision = form.get("decision") === "approve" ? "approve" : "deny";
99
+ const response = await transport.fetch(device ? `/oauth/device/${decision}` : requestPath(id, decision), {
100
+ method: "POST",
101
+ headers: { "content-type": "application/json", accept: "application/json" },
102
+ body: JSON.stringify({
103
+ ...(device ? { user_code: id.slice(7) } : {}),
104
+ ...(form.get("scope") === "ontology:read" ? { scope: "ontology:read" } : {}),
105
+ }),
106
+ });
107
+ const payload = await readJson(response);
108
+ if (response.status === 404) {
109
+ return NextResponse.redirect(new URL(`${CONSENT_PAGE}?expired=1`, request.url), 303);
110
+ }
111
+ if (!response.ok) {
112
+ throw apiErrorFrom(response.status, payload, "The ontology service refused the decision.");
113
+ }
114
+ if (device) {
115
+ return oauthCompletion({ approved: decision === "approve" });
116
+ }
117
+ const redirect = payload?.redirect;
118
+ if (typeof redirect !== "string" || !URL.canParse(redirect)) {
119
+ throw apiErrorFrom(502, payload, "The ontology service answered without a redirect.");
120
+ }
121
+ if (decision === "approve" && loopbackRedirect(redirect)) {
122
+ return oauthCompletion({ redirect });
123
+ }
124
+ // 303: the agent's redirect URI receives a GET, whatever method brought the browser here.
125
+ return NextResponse.redirect(redirect, 303);
126
+ },
127
+ }),
128
+ };
129
+ }
@@ -0,0 +1,10 @@
1
+ /** Convert each character once, including acronym boundaries such as HTTPClient. */
2
+ export declare function toSnakeCase(name: string): string;
3
+ export declare function toCamelCase(name: string): string;
4
+ /**
5
+ * camelCase that keeps an `_` before a digit, so `toSnakeCase` restores the name: `sales_2025`
6
+ * stays `sales_2025` and `wb2_sales` becomes `wb2Sales`. Model-level metric query names, whose
7
+ * first segment is a declared snake_case entity name, use it; records and per-entity metric
8
+ * queries keep `toCamelCase`.
9
+ */
10
+ export declare function toReversibleCamelCase(name: string): string;
@@ -0,0 +1,38 @@
1
+ /** Ontology identifiers in TypeScript and their canonical transport spelling. */
2
+ function isUppercase(letter) {
3
+ return letter !== undefined && letter >= "A" && letter <= "Z";
4
+ }
5
+ function isLowercase(letter) {
6
+ return letter !== undefined && letter >= "a" && letter <= "z";
7
+ }
8
+ /** Convert each character once, including acronym boundaries such as HTTPClient. */
9
+ export function toSnakeCase(name) {
10
+ if (name.toLowerCase() === name) {
11
+ return name;
12
+ }
13
+ const characters = [];
14
+ for (let index = 0; index < name.length; index++) {
15
+ const current = name[index];
16
+ const previous = name[index - 1];
17
+ const next = name[index + 1];
18
+ const followsWord = isLowercase(previous) || (previous !== undefined && previous >= "0" && previous <= "9");
19
+ const endsAcronym = isUppercase(previous) && isLowercase(next);
20
+ if (isUppercase(current) && (followsWord || endsAcronym)) {
21
+ characters.push("_");
22
+ }
23
+ characters.push(current);
24
+ }
25
+ return characters.join("").toLowerCase();
26
+ }
27
+ export function toCamelCase(name) {
28
+ return name.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase());
29
+ }
30
+ /**
31
+ * camelCase that keeps an `_` before a digit, so `toSnakeCase` restores the name: `sales_2025`
32
+ * stays `sales_2025` and `wb2_sales` becomes `wb2Sales`. Model-level metric query names, whose
33
+ * first segment is a declared snake_case entity name, use it; records and per-entity metric
34
+ * queries keep `toCamelCase`.
35
+ */
36
+ export function toReversibleCamelCase(name) {
37
+ return name.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
38
+ }
@@ -0,0 +1,34 @@
1
+ import { type OntologyTransport } from "./ontology.js";
2
+ import type { Fetch } from "./sessions.js";
3
+ /** Every ontology route lives under this path, beside the app on the same origin. */
4
+ export declare const ONTOLOGY_BASE_PATH = "/api/ontology";
5
+ /**
6
+ * How long an action call waits. The service answers an action within its own bounds: up to 5 s
7
+ * for a write slot, 60 s for `prepare` and 60 s for the transaction, then its workflow starts.
8
+ * Waiting longer means a slow action returns its result instead of an error while it still runs.
9
+ */
10
+ export declare const ACTION_TIMEOUT_MS = 150000;
11
+ /** How long a metric query waits; the service waits up to 60 s for SegmentStream's answer. */
12
+ export declare const METRIC_QUERY_TIMEOUT_MS = 75000;
13
+ /** How long every other request waits. */
14
+ export declare const REQUEST_TIMEOUT_MS = 30000;
15
+ /** The caller's bearer token for the ontology service. */
16
+ export interface OntologyCredentials {
17
+ /** The token for this request, or `null` when nobody is signed in. */
18
+ readonly token: () => Promise<string | null>;
19
+ /** Forgets a token the service rejected, so `token` returns a fresh one. */
20
+ readonly rejected?: (token: string) => void;
21
+ }
22
+ export interface OntologyTransportOptions {
23
+ readonly fetch: Fetch;
24
+ /** `SEGMENTSTREAM_ONTOLOGY_URL`: where the service runs when it is not beside the app. */
25
+ readonly ontologyUrl: string | undefined;
26
+ readonly credentials: OntologyCredentials;
27
+ }
28
+ /**
29
+ * Server-side requests to the ontology service with the signed-in user's token. Deployed, the
30
+ * service shares the app's origin; `SEGMENTSTREAM_ONTOLOGY_URL` points elsewhere, for example at
31
+ * `http://localhost:3100` or the deployed app during local development.
32
+ */
33
+ export declare function createOntologyTransport(options: OntologyTransportOptions): OntologyTransport;
34
+ export declare function ontologyOrigin(configured: string | undefined): Promise<string>;
@@ -0,0 +1,130 @@
1
+ import { headers } from "next/headers";
2
+ import { isLoopbackHostname } from "./cookies.js";
3
+ import { SegmentStreamApiError } from "./errors.js";
4
+ import { EARLIER_QUERY_METRICS } from "./legacy.js";
5
+ import { QUERY_METRICS_ROUTE } from "./ontology.js";
6
+ /** Every ontology route lives under this path, beside the app on the same origin. */
7
+ export const ONTOLOGY_BASE_PATH = "/api/ontology";
8
+ /**
9
+ * How long an action call waits. The service answers an action within its own bounds: up to 5 s
10
+ * for a write slot, 60 s for `prepare` and 60 s for the transaction, then its workflow starts.
11
+ * Waiting longer means a slow action returns its result instead of an error while it still runs.
12
+ */
13
+ export const ACTION_TIMEOUT_MS = 150_000;
14
+ /** How long a metric query waits; the service waits up to 60 s for SegmentStream's answer. */
15
+ export const METRIC_QUERY_TIMEOUT_MS = 75_000;
16
+ /** How long every other request waits. */
17
+ export const REQUEST_TIMEOUT_MS = 30_000;
18
+ const ACTION_PATH = /^\/[^/]+\/actions\/[^/]+$/;
19
+ const METRIC_QUERY_PATH = new RegExp(`^(?:/[^/]+)?/(?:${QUERY_METRICS_ROUTE}|${EARLIER_QUERY_METRICS})$`);
20
+ /** A caller's `signal` can shorten these, never extend them. */
21
+ function timeoutOf(path, init) {
22
+ if (init.method?.toUpperCase() !== "POST") {
23
+ return REQUEST_TIMEOUT_MS;
24
+ }
25
+ return ACTION_PATH.test(path)
26
+ ? ACTION_TIMEOUT_MS
27
+ : METRIC_QUERY_PATH.test(path)
28
+ ? METRIC_QUERY_TIMEOUT_MS
29
+ : REQUEST_TIMEOUT_MS;
30
+ }
31
+ /**
32
+ * Server-side requests to the ontology service with the signed-in user's token. Deployed, the
33
+ * service shares the app's origin; `SEGMENTSTREAM_ONTOLOGY_URL` points elsewhere, for example at
34
+ * `http://localhost:3100` or the deployed app during local development.
35
+ */
36
+ export function createOntologyTransport(options) {
37
+ async function send(url, token, init, timeoutMs) {
38
+ const requestHeaders = new Headers(init.headers);
39
+ const timeout = AbortSignal.timeout(timeoutMs);
40
+ requestHeaders.set("authorization", `Bearer ${token}`);
41
+ if (token.startsWith("sso_")) {
42
+ // Keep the browser's public app-session resource when a local proxy uses another origin.
43
+ const incoming = await headers();
44
+ const host = incoming.get("x-forwarded-host") ?? incoming.get("host");
45
+ if (host) {
46
+ requestHeaders.set("x-forwarded-host", host);
47
+ requestHeaders.set("x-forwarded-proto", incoming.get("x-forwarded-proto") ?? "https");
48
+ }
49
+ }
50
+ try {
51
+ return await options.fetch(url, {
52
+ ...init,
53
+ headers: requestHeaders,
54
+ cache: "no-store",
55
+ redirect: "error",
56
+ signal: init.signal ? AbortSignal.any([init.signal, timeout]) : timeout,
57
+ });
58
+ }
59
+ catch (error) {
60
+ if (init.signal?.aborted) {
61
+ throw error;
62
+ }
63
+ throw new SegmentStreamApiError({
64
+ code: "ontology_unreachable",
65
+ status: 503,
66
+ detail: `The ontology service at ${new URL(url).origin} could not be reached.`,
67
+ retryable: true,
68
+ cause: error,
69
+ });
70
+ }
71
+ }
72
+ return {
73
+ async fetch(path, init) {
74
+ const token = await options.credentials.token();
75
+ if (token === null) {
76
+ throw new SegmentStreamApiError({
77
+ code: "app_session_missing",
78
+ status: 401,
79
+ detail: "No signed-in user for this request; call the ontology from Server Components, Route Handlers, or Server Functions behind the SegmentStream proxy.",
80
+ });
81
+ }
82
+ const url = `${await ontologyOrigin(options.ontologyUrl)}${ONTOLOGY_BASE_PATH}${path}`;
83
+ const timeoutMs = timeoutOf(path, init);
84
+ const response = await send(url, token, init, timeoutMs);
85
+ if (response.status !== 401 || options.credentials.rejected === undefined) {
86
+ return response;
87
+ }
88
+ // A cached token the service no longer accepts: retry once with a fresh one.
89
+ options.credentials.rejected(token);
90
+ const fresh = await options.credentials.token();
91
+ if (fresh === null || fresh === token) {
92
+ return response;
93
+ }
94
+ await response.body?.cancel();
95
+ return send(url, fresh, init, timeoutMs);
96
+ },
97
+ };
98
+ }
99
+ export async function ontologyOrigin(configured) {
100
+ if (configured !== undefined) {
101
+ return new URL(configured).origin;
102
+ }
103
+ // Vercel sets Host to the domain the request arrived on, which routes /api/ontology to the service.
104
+ const headerStore = await headers();
105
+ const host = (headerStore.get("x-forwarded-host") ?? headerStore.get("host"))
106
+ ?.split(",")[0]
107
+ ?.trim();
108
+ if (host === undefined || host === "") {
109
+ throw new SegmentStreamApiError({
110
+ code: "ontology_url_unknown",
111
+ status: 500,
112
+ detail: "The request has no Host header to reach the ontology service; set SEGMENTSTREAM_ONTOLOGY_URL.",
113
+ });
114
+ }
115
+ const forwarded = headerStore.get("x-forwarded-proto")?.split(",")[0]?.trim();
116
+ const protocol = forwarded === "http" || forwarded === "https"
117
+ ? forwarded
118
+ : isLoopbackHostname(hostnameOf(host))
119
+ ? "http"
120
+ : "https";
121
+ return new URL(`${protocol}://${host}`).origin;
122
+ }
123
+ function hostnameOf(host) {
124
+ try {
125
+ return new URL(`http://${host}`).hostname;
126
+ }
127
+ catch {
128
+ return "";
129
+ }
130
+ }
@@ -0,0 +1,66 @@
1
+ import type { Edit, JsonRecord, OntologyQuery, Where } from "./ontology.js";
2
+ interface CatalogProperty {
3
+ readonly name: string;
4
+ readonly type: string;
5
+ readonly options?: ReadonlyArray<{
6
+ readonly value: string;
7
+ }>;
8
+ }
9
+ interface CatalogRelationship {
10
+ readonly name: string;
11
+ readonly to: string;
12
+ readonly inverse: string;
13
+ readonly cardinality: string;
14
+ /** Present when either end is warehouse-backed: the relationship is matched by value. */
15
+ readonly on?: unknown;
16
+ }
17
+ interface CatalogEntity {
18
+ readonly name: string;
19
+ readonly properties: ReadonlyArray<CatalogProperty>;
20
+ readonly relationships: ReadonlyArray<CatalogRelationship>;
21
+ readonly actions?: ReadonlyArray<{
22
+ readonly name: string;
23
+ readonly input: JsonSchema;
24
+ }>;
25
+ }
26
+ interface JsonSchema {
27
+ readonly $ref?: string;
28
+ readonly $defs?: Readonly<Record<string, JsonSchema>>;
29
+ readonly properties?: Readonly<Record<string, JsonSchema>>;
30
+ readonly items?: JsonSchema;
31
+ readonly anyOf?: ReadonlyArray<JsonSchema>;
32
+ readonly oneOf?: ReadonlyArray<JsonSchema>;
33
+ readonly enum?: ReadonlyArray<unknown>;
34
+ readonly const?: unknown;
35
+ readonly additionalProperties?: boolean | JsonSchema;
36
+ readonly required?: ReadonlyArray<string>;
37
+ }
38
+ export interface WireCatalog {
39
+ readonly hash?: string;
40
+ readonly entities: ReadonlyArray<CatalogEntity>;
41
+ }
42
+ /** Whether a generated action of `user` answers with the updated user record. */
43
+ export declare function returnsUserRecord(entity: string, action: string): boolean;
44
+ /** Catalog metadata distinguishes relationship filters from opaque JSON property values. */
45
+ export declare function encodeWhere(input: {
46
+ readonly where: Where | undefined;
47
+ readonly entity: string;
48
+ readonly getCatalog: () => Promise<WireCatalog>;
49
+ }): Promise<Where | undefined>;
50
+ export declare function queryToWire(query: OntologyQuery<JsonRecord, readonly string[] | undefined>): unknown;
51
+ /**
52
+ * `include` with snake_case names, as an object: a list of names, `["assignedOwner"]`, becomes
53
+ * `{ assigned_owner: true }`, which every version of the ontology service accepts.
54
+ */
55
+ export declare function includeToWire(include: NonNullable<OntologyQuery["include"]>): Readonly<Record<string, unknown>>;
56
+ /** Decode record fields and included relationships without descending into JSON properties. */
57
+ export declare function recordFromWire<A = unknown>(value: unknown, entity: string, catalog: WireCatalog): A;
58
+ export declare function actionInputToWire(given: unknown, entity: string, action: string, catalog: WireCatalog): unknown;
59
+ export declare function responseFromWire(input: {
60
+ readonly method: "GET" | "POST";
61
+ readonly path: string;
62
+ readonly payload: unknown;
63
+ }): unknown;
64
+ /** Decode current edits using the current catalog; stored historical changes retain their names. */
65
+ export declare function editsFromWire(edits: ReadonlyArray<Edit>, catalog: WireCatalog): ReadonlyArray<Edit>;
66
+ export {};