@stapel/core 0.2.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/dist/analytics/context.d.ts +9 -0
  4. package/dist/analytics/context.d.ts.map +1 -0
  5. package/dist/analytics/context.js +14 -0
  6. package/dist/analytics/context.js.map +1 -0
  7. package/dist/analytics/createAnalytics.d.ts +10 -0
  8. package/dist/analytics/createAnalytics.d.ts.map +1 -0
  9. package/dist/analytics/createAnalytics.js +273 -0
  10. package/dist/analytics/createAnalytics.js.map +1 -0
  11. package/dist/analytics/flow.d.ts +10 -0
  12. package/dist/analytics/flow.d.ts.map +1 -0
  13. package/dist/analytics/flow.js +10 -0
  14. package/dist/analytics/flow.js.map +1 -0
  15. package/dist/analytics/hash.d.ts +3 -0
  16. package/dist/analytics/hash.d.ts.map +1 -0
  17. package/dist/analytics/hash.js +12 -0
  18. package/dist/analytics/hash.js.map +1 -0
  19. package/dist/analytics/pii.d.ts +9 -0
  20. package/dist/analytics/pii.d.ts.map +1 -0
  21. package/dist/analytics/pii.js +52 -0
  22. package/dist/analytics/pii.js.map +1 -0
  23. package/dist/analytics/providers.d.ts +28 -0
  24. package/dist/analytics/providers.d.ts.map +1 -0
  25. package/dist/analytics/providers.js +82 -0
  26. package/dist/analytics/providers.js.map +1 -0
  27. package/dist/analytics/types.d.ts +94 -0
  28. package/dist/analytics/types.d.ts.map +1 -0
  29. package/dist/analytics/types.js +7 -0
  30. package/dist/analytics/types.js.map +1 -0
  31. package/dist/client.d.ts +49 -0
  32. package/dist/client.d.ts.map +1 -0
  33. package/dist/client.js +135 -0
  34. package/dist/client.js.map +1 -0
  35. package/dist/config.d.ts +32 -0
  36. package/dist/config.d.ts.map +1 -0
  37. package/dist/config.js +28 -0
  38. package/dist/config.js.map +1 -0
  39. package/dist/errors.d.ts +33 -0
  40. package/dist/errors.d.ts.map +1 -0
  41. package/dist/errors.js +46 -0
  42. package/dist/errors.js.map +1 -0
  43. package/dist/i18n.d.ts +51 -0
  44. package/dist/i18n.d.ts.map +1 -0
  45. package/dist/i18n.js +90 -0
  46. package/dist/i18n.js.map +1 -0
  47. package/dist/index.d.ts +22 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +19 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/query.d.ts +42 -0
  52. package/dist/query.d.ts.map +1 -0
  53. package/dist/query.js +95 -0
  54. package/dist/query.js.map +1 -0
  55. package/dist/storage.d.ts +16 -0
  56. package/dist/storage.d.ts.map +1 -0
  57. package/dist/storage.js +65 -0
  58. package/dist/storage.js.map +1 -0
  59. package/dist/useBreakpoint.d.ts +8 -0
  60. package/dist/useBreakpoint.d.ts.map +1 -0
  61. package/dist/useBreakpoint.js +22 -0
  62. package/dist/useBreakpoint.js.map +1 -0
  63. package/dist/verification.d.ts +31 -0
  64. package/dist/verification.d.ts.map +1 -0
  65. package/dist/verification.js +20 -0
  66. package/dist/verification.js.map +1 -0
  67. package/package.json +68 -0
  68. package/src/analytics/context.ts +20 -0
  69. package/src/analytics/createAnalytics.ts +310 -0
  70. package/src/analytics/flow.ts +19 -0
  71. package/src/analytics/hash.ts +16 -0
  72. package/src/analytics/pii.ts +66 -0
  73. package/src/analytics/providers.ts +108 -0
  74. package/src/analytics/types.ts +105 -0
  75. package/src/client.ts +206 -0
  76. package/src/config.tsx +62 -0
  77. package/src/errors.ts +70 -0
  78. package/src/i18n.tsx +147 -0
  79. package/src/index.ts +72 -0
  80. package/src/query.ts +151 -0
  81. package/src/storage.ts +76 -0
  82. package/src/useBreakpoint.ts +27 -0
  83. package/src/verification.ts +48 -0
  84. package/tsconfig.json +26 -0
package/src/client.ts ADDED
@@ -0,0 +1,206 @@
1
+ import { parseErrorEnvelope } from "./errors.js";
2
+ import {
3
+ extractVerificationChallenge,
4
+ VERIFICATION_TOKEN_HEADER,
5
+ type VerificationChallengeHandler,
6
+ } from "./verification.js";
7
+
8
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
9
+
10
+ export interface StapelRequestOptions {
11
+ readonly method?: HttpMethod;
12
+ /** JSON-serialized unless it is a `BodyInit` (FormData, Blob, string…). */
13
+ readonly body?: unknown;
14
+ readonly headers?: Record<string, string>;
15
+ /** Appended to the URL; `undefined` values are skipped. */
16
+ readonly query?: Record<string, string | number | boolean | undefined>;
17
+ readonly signal?: AbortSignal;
18
+ }
19
+
20
+ export interface StapelClient {
21
+ readonly baseUrl: string;
22
+ request<T>(path: string, options?: StapelRequestOptions): Promise<T>;
23
+ get<T>(path: string, options?: Omit<StapelRequestOptions, "method" | "body">): Promise<T>;
24
+ post<T>(path: string, body?: unknown, options?: Omit<StapelRequestOptions, "method" | "body">): Promise<T>;
25
+ put<T>(path: string, body?: unknown, options?: Omit<StapelRequestOptions, "method" | "body">): Promise<T>;
26
+ patch<T>(path: string, body?: unknown, options?: Omit<StapelRequestOptions, "method" | "body">): Promise<T>;
27
+ delete<T>(path: string, options?: Omit<StapelRequestOptions, "method" | "body">): Promise<T>;
28
+ }
29
+
30
+ export interface StapelClientOptions {
31
+ /** e.g. `https://api.example.com` or `/api`. */
32
+ readonly baseUrl: string;
33
+ /** Auth seam: current access token (attached as `Authorization: Bearer`). */
34
+ readonly getToken?: () =>
35
+ | string
36
+ | null
37
+ | undefined
38
+ | Promise<string | null | undefined>;
39
+ /**
40
+ * Refresh seam: called once per request on a 401. Return the new access
41
+ * token to retry the request with it; return null/undefined to give up
42
+ * (the 401 is then thrown as `StapelApiError`).
43
+ */
44
+ readonly onAuthRefresh?: () => Promise<string | null | undefined>;
45
+ /**
46
+ * Step-up verification seam: called when a 403 body carries a
47
+ * `verification` challenge. On `{retry: true}` the original request is
48
+ * retried exactly once, with `X-Verification-Token` when a token is given.
49
+ */
50
+ readonly onVerificationChallenge?: VerificationChallengeHandler;
51
+ /** Merged into every request (overridable per request). */
52
+ readonly defaultHeaders?: Record<string, string>;
53
+ /** Injectable fetch (tests, SSR, instrumentation). Default: global fetch. */
54
+ readonly fetch?: typeof globalThis.fetch;
55
+ }
56
+
57
+ function isBodyInit(body: unknown): body is BodyInit {
58
+ return (
59
+ typeof body === "string" ||
60
+ (typeof Blob !== "undefined" && body instanceof Blob) ||
61
+ (typeof FormData !== "undefined" && body instanceof FormData) ||
62
+ (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) ||
63
+ (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) ||
64
+ (typeof ReadableStream !== "undefined" && body instanceof ReadableStream)
65
+ );
66
+ }
67
+
68
+ function buildUrl(
69
+ baseUrl: string,
70
+ path: string,
71
+ query?: StapelRequestOptions["query"]
72
+ ): string {
73
+ const base = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
74
+ const suffix = path.startsWith("/") ? path : `/${path}`;
75
+ let url = `${base}${suffix}`;
76
+ if (query) {
77
+ const search = new URLSearchParams();
78
+ for (const [key, value] of Object.entries(query)) {
79
+ if (value !== undefined) search.set(key, String(value));
80
+ }
81
+ const qs = search.toString();
82
+ if (qs.length > 0) url += `?${qs}`;
83
+ }
84
+ return url;
85
+ }
86
+
87
+ async function parseBody(response: Response): Promise<unknown> {
88
+ if (response.status === 204 || response.status === 205) return undefined;
89
+ const contentType = response.headers.get("content-type") ?? "";
90
+ const text = await response.text();
91
+ if (text.length === 0) return undefined;
92
+ if (contentType.includes("json")) {
93
+ try {
94
+ return JSON.parse(text) as unknown;
95
+ } catch {
96
+ return text;
97
+ }
98
+ }
99
+ try {
100
+ return JSON.parse(text) as unknown;
101
+ } catch {
102
+ return text;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Typed fetch wrapper around the Stapel API conventions: JSON in/out, bearer
108
+ * auth with a refresh seam, the `{localizable_error, error, params}` error
109
+ * envelope, and verification-403 interception (see `StapelClientOptions`).
110
+ */
111
+ export function createStapelClient(options: StapelClientOptions): StapelClient {
112
+ const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
113
+
114
+ async function request<T>(
115
+ path: string,
116
+ requestOptions: StapelRequestOptions = {}
117
+ ): Promise<T> {
118
+ const method = requestOptions.method ?? "GET";
119
+ const url = buildUrl(options.baseUrl, path, requestOptions.query);
120
+
121
+ let overrideToken: string | undefined;
122
+ let verificationToken: string | undefined;
123
+ let triedRefresh = false;
124
+ let triedVerification = false;
125
+
126
+ for (;;) {
127
+ const headers = new Headers(options.defaultHeaders);
128
+ if (requestOptions.headers) {
129
+ for (const [key, value] of Object.entries(requestOptions.headers)) {
130
+ headers.set(key, value);
131
+ }
132
+ }
133
+ const token = overrideToken ?? (await options.getToken?.());
134
+ if (token != null && token.length > 0 && !headers.has("authorization")) {
135
+ headers.set("Authorization", `Bearer ${token}`);
136
+ }
137
+ if (verificationToken !== undefined) {
138
+ headers.set(VERIFICATION_TOKEN_HEADER, verificationToken);
139
+ }
140
+
141
+ let body: BodyInit | undefined;
142
+ if (requestOptions.body !== undefined) {
143
+ if (isBodyInit(requestOptions.body)) {
144
+ body = requestOptions.body;
145
+ } else {
146
+ body = JSON.stringify(requestOptions.body);
147
+ if (!headers.has("content-type")) {
148
+ headers.set("Content-Type", "application/json");
149
+ }
150
+ }
151
+ }
152
+
153
+ const requestInit: RequestInit = { method, headers };
154
+ if (body !== undefined) requestInit.body = body;
155
+ if (requestOptions.signal) requestInit.signal = requestOptions.signal;
156
+ const response = await fetchImpl(url, requestInit);
157
+
158
+ if (response.ok) {
159
+ return (await parseBody(response)) as T;
160
+ }
161
+
162
+ const errorBody = await parseBody(response);
163
+
164
+ if (response.status === 401 && options.onAuthRefresh && !triedRefresh) {
165
+ triedRefresh = true;
166
+ const refreshed = await options.onAuthRefresh();
167
+ if (refreshed != null && refreshed.length > 0) {
168
+ overrideToken = refreshed;
169
+ continue;
170
+ }
171
+ }
172
+
173
+ if (
174
+ response.status === 403 &&
175
+ options.onVerificationChallenge &&
176
+ !triedVerification
177
+ ) {
178
+ const challenge = extractVerificationChallenge(errorBody);
179
+ if (challenge) {
180
+ triedVerification = true;
181
+ const outcome = await options.onVerificationChallenge(challenge);
182
+ if (outcome.retry) {
183
+ if (outcome.token !== undefined) {
184
+ verificationToken = outcome.token;
185
+ }
186
+ continue;
187
+ }
188
+ }
189
+ }
190
+
191
+ throw parseErrorEnvelope(response.status, errorBody);
192
+ }
193
+ }
194
+
195
+ return {
196
+ baseUrl: options.baseUrl,
197
+ request,
198
+ get: (path, opts) => request(path, { ...opts, method: "GET" }),
199
+ post: (path, body, opts) =>
200
+ request(path, { ...opts, method: "POST", body }),
201
+ put: (path, body, opts) => request(path, { ...opts, method: "PUT", body }),
202
+ patch: (path, body, opts) =>
203
+ request(path, { ...opts, method: "PATCH", body }),
204
+ delete: (path, opts) => request(path, { ...opts, method: "DELETE" }),
205
+ };
206
+ }
package/src/config.tsx ADDED
@@ -0,0 +1,62 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { ReactElement, ReactNode } from "react";
3
+ import type { StapelClient } from "./client.js";
4
+ import { AnalyticsContext } from "./analytics/context.js";
5
+ import type { Analytics } from "./analytics/types.js";
6
+
7
+ /**
8
+ * App-level Stapel configuration. `clients` allows per-module client
9
+ * overrides — the client-injection fork-resolution seam of
10
+ * frontend-standard §7.2: a divergent backend gets its own generated client
11
+ * injected into the same package machines.
12
+ */
13
+ export interface StapelConfig {
14
+ /** Default API client used by all `@stapel/<module>-react` packages. */
15
+ readonly client: StapelClient;
16
+ /** Per-module overrides, keyed by module name (e.g. `"auth"`). */
17
+ readonly clients?: Readonly<Record<string, StapelClient>>;
18
+ }
19
+
20
+ const StapelConfigContext = createContext<StapelConfig | null>(null);
21
+
22
+ export function StapelConfigProvider(props: {
23
+ config: StapelConfig;
24
+ /**
25
+ * Optional analytics facade (analytics-standard §2); when provided,
26
+ * `useAnalytics()` works anywhere below. Omitting it keeps the previous
27
+ * behaviour.
28
+ */
29
+ analytics?: Analytics;
30
+ children: ReactNode;
31
+ }): ReactElement {
32
+ return (
33
+ <StapelConfigContext.Provider value={props.config}>
34
+ <AnalyticsContext.Provider value={props.analytics ?? null}>
35
+ {props.children}
36
+ </AnalyticsContext.Provider>
37
+ </StapelConfigContext.Provider>
38
+ );
39
+ }
40
+
41
+ export function useStapelConfig(): StapelConfig {
42
+ const config = useContext(StapelConfigContext);
43
+ if (config === null) {
44
+ throw new Error(
45
+ "useStapelConfig must be used within a <StapelConfigProvider>"
46
+ );
47
+ }
48
+ return config;
49
+ }
50
+
51
+ /**
52
+ * Resolve the API client for a module: the per-module override when
53
+ * configured, otherwise the default client.
54
+ */
55
+ export function useStapelClient(module?: string): StapelClient {
56
+ const config = useStapelConfig();
57
+ if (module !== undefined) {
58
+ const override = config.clients?.[module];
59
+ if (override) return override;
60
+ }
61
+ return config.client;
62
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The Stapel backend error envelope:
3
+ * `{ localizable_error: "auth.otp.invalid", error: "Invalid OTP", params: {...} }`
4
+ * `localizable_error` is an i18n key; `params` feed `{param}` interpolation.
5
+ */
6
+ export interface StapelErrorEnvelope {
7
+ readonly localizable_error?: string;
8
+ readonly error?: string;
9
+ readonly params?: Record<string, unknown>;
10
+ }
11
+
12
+ export class StapelApiError extends Error {
13
+ /** i18n key from `localizable_error` (fallback: `stapel.http.<status>`). */
14
+ readonly code: string;
15
+ /** Interpolation params for the i18n key. */
16
+ readonly params: Readonly<Record<string, unknown>>;
17
+ /** HTTP status code. */
18
+ readonly status: number;
19
+ /** Raw (parsed) response body, for diagnostics and extensions. */
20
+ readonly body: unknown;
21
+
22
+ constructor(args: {
23
+ code: string;
24
+ message: string;
25
+ params?: Record<string, unknown>;
26
+ status: number;
27
+ body?: unknown;
28
+ }) {
29
+ super(args.message);
30
+ this.name = "StapelApiError";
31
+ this.code = args.code;
32
+ this.params = args.params ?? {};
33
+ this.status = args.status;
34
+ this.body = args.body;
35
+ }
36
+ }
37
+
38
+ function isRecord(value: unknown): value is Record<string, unknown> {
39
+ return typeof value === "object" && value !== null && !Array.isArray(value);
40
+ }
41
+
42
+ /**
43
+ * Parse a failed response body (already JSON-decoded; may be anything) into
44
+ * a `StapelApiError`. Tolerant of non-envelope bodies.
45
+ */
46
+ export function parseErrorEnvelope(
47
+ status: number,
48
+ body: unknown
49
+ ): StapelApiError {
50
+ const fallbackCode = `stapel.http.${String(status)}`;
51
+ if (!isRecord(body)) {
52
+ return new StapelApiError({
53
+ code: fallbackCode,
54
+ message: `Request failed with status ${String(status)}`,
55
+ status,
56
+ body,
57
+ });
58
+ }
59
+ const code =
60
+ typeof body["localizable_error"] === "string" &&
61
+ body["localizable_error"].length > 0
62
+ ? body["localizable_error"]
63
+ : fallbackCode;
64
+ const message =
65
+ typeof body["error"] === "string" && body["error"].length > 0
66
+ ? body["error"]
67
+ : code;
68
+ const params = isRecord(body["params"]) ? body["params"] : {};
69
+ return new StapelApiError({ code, message, params, status, body });
70
+ }
package/src/i18n.tsx ADDED
@@ -0,0 +1,147 @@
1
+ import { createContext, useContext, useSyncExternalStore } from "react";
2
+ import type { ReactElement, ReactNode } from "react";
3
+
4
+ /** Flat key → string dictionary, e.g. `{"auth.otp.invalid": "Invalid code"}`. */
5
+ export type I18nDictionary = Record<string, string>;
6
+
7
+ /**
8
+ * Async locale loader seam. Point it at the stapel-translate pair:
9
+ * `loadLocale: (locale) => translateClient.resolve(locale)` — the engine
10
+ * calls it once per locale and caches the result as a bundle.
11
+ */
12
+ export type LocaleLoader = (locale: string) => Promise<I18nDictionary>;
13
+
14
+ export type TranslateFn = (
15
+ key: string,
16
+ params?: Record<string, unknown>
17
+ ) => string;
18
+
19
+ export interface I18nEngine {
20
+ /** Current locale. */
21
+ readonly locale: string;
22
+ /** Translate a key; missing keys fall back to the key itself. */
23
+ t: TranslateFn;
24
+ /** Switch locale; loads it via `loadLocale` when not already registered. */
25
+ setLocale(locale: string): Promise<void>;
26
+ /** Register a static bundle (packages register their keys this way). */
27
+ registerBundle(locale: string, bundle: I18nDictionary): void;
28
+ /** Subscribe to engine changes (locale switches, bundle registration). */
29
+ subscribe(listener: () => void): () => void;
30
+ /** Monotonic change counter (for useSyncExternalStore). */
31
+ getVersion(): number;
32
+ }
33
+
34
+ /** `{param}` interpolation. Unknown params are left as-is. */
35
+ export function interpolate(
36
+ template: string,
37
+ params?: Record<string, unknown>
38
+ ): string {
39
+ if (!params) return template;
40
+ return template.replace(/\{([\w.]+)\}/g, (match, name: string) =>
41
+ Object.prototype.hasOwnProperty.call(params, name)
42
+ ? String(params[name])
43
+ : match
44
+ );
45
+ }
46
+
47
+ export interface CreateI18nOptions {
48
+ /** Initial locale. */
49
+ readonly locale: string;
50
+ /** Static bundles, keyed by locale. */
51
+ readonly bundles?: Readonly<Record<string, I18nDictionary>>;
52
+ /** Async loader for locales not covered by static bundles. */
53
+ readonly loadLocale?: LocaleLoader;
54
+ }
55
+
56
+ /**
57
+ * Minimal i18n engine: dictionaries per locale, `{param}` interpolation,
58
+ * static bundles + async loader, missing-key fallback to the key itself
59
+ * (frontend-standard §4.2 — user-facing strings are always keys).
60
+ */
61
+ export function createI18n(options: CreateI18nOptions): I18nEngine {
62
+ const dictionaries = new Map<string, I18nDictionary>();
63
+ const loadedLocales = new Set<string>();
64
+ const listeners = new Set<() => void>();
65
+ let locale = options.locale;
66
+ let version = 0;
67
+
68
+ if (options.bundles) {
69
+ for (const [bundleLocale, bundle] of Object.entries(options.bundles)) {
70
+ dictionaries.set(bundleLocale, { ...bundle });
71
+ }
72
+ }
73
+
74
+ function notify(): void {
75
+ version += 1;
76
+ for (const listener of listeners) listener();
77
+ }
78
+
79
+ async function ensureLoaded(nextLocale: string): Promise<void> {
80
+ if (!options.loadLocale || loadedLocales.has(nextLocale)) return;
81
+ loadedLocales.add(nextLocale);
82
+ const bundle = await options.loadLocale(nextLocale);
83
+ const existing = dictionaries.get(nextLocale) ?? {};
84
+ dictionaries.set(nextLocale, { ...existing, ...bundle });
85
+ }
86
+
87
+ const engine: I18nEngine = {
88
+ get locale(): string {
89
+ return locale;
90
+ },
91
+ t: (key, params) => {
92
+ const template = dictionaries.get(locale)?.[key];
93
+ if (template === undefined) return key;
94
+ return interpolate(template, params);
95
+ },
96
+ setLocale: async (nextLocale) => {
97
+ await ensureLoaded(nextLocale);
98
+ locale = nextLocale;
99
+ notify();
100
+ },
101
+ registerBundle: (bundleLocale, bundle) => {
102
+ const existing = dictionaries.get(bundleLocale) ?? {};
103
+ dictionaries.set(bundleLocale, { ...existing, ...bundle });
104
+ notify();
105
+ },
106
+ subscribe: (listener) => {
107
+ listeners.add(listener);
108
+ return () => {
109
+ listeners.delete(listener);
110
+ };
111
+ },
112
+ getVersion: () => version,
113
+ };
114
+
115
+ return engine;
116
+ }
117
+
118
+ const I18nContext = createContext<I18nEngine | null>(null);
119
+
120
+ export function I18nProvider(props: {
121
+ i18n: I18nEngine;
122
+ children: ReactNode;
123
+ }): ReactElement {
124
+ return (
125
+ <I18nContext.Provider value={props.i18n}>
126
+ {props.children}
127
+ </I18nContext.Provider>
128
+ );
129
+ }
130
+
131
+ export function useI18n(): I18nEngine {
132
+ const engine = useContext(I18nContext);
133
+ if (engine === null) {
134
+ throw new Error("useI18n must be used within an <I18nProvider>");
135
+ }
136
+ return engine;
137
+ }
138
+
139
+ /**
140
+ * Reactive translate function: re-renders on locale switches and bundle
141
+ * registration.
142
+ */
143
+ export function useT(): TranslateFn {
144
+ const engine = useI18n();
145
+ useSyncExternalStore(engine.subscribe, engine.getVersion, engine.getVersion);
146
+ return engine.t;
147
+ }
package/src/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ // fetch + error envelope
2
+ export { createStapelClient } from "./client.js";
3
+ export type {
4
+ StapelClient,
5
+ StapelClientOptions,
6
+ StapelRequestOptions,
7
+ HttpMethod,
8
+ } from "./client.js";
9
+ export { StapelApiError, parseErrorEnvelope } from "./errors.js";
10
+ export type { StapelErrorEnvelope } from "./errors.js";
11
+
12
+ // verification-403 interception seam
13
+ export {
14
+ extractVerificationChallenge,
15
+ VERIFICATION_TOKEN_HEADER,
16
+ } from "./verification.js";
17
+ export type {
18
+ VerificationChallenge,
19
+ VerificationOutcome,
20
+ VerificationChallengeHandler,
21
+ } from "./verification.js";
22
+
23
+ // config provider + client injection
24
+ export {
25
+ StapelConfigProvider,
26
+ useStapelConfig,
27
+ useStapelClient,
28
+ } from "./config.js";
29
+ export type { StapelConfig } from "./config.js";
30
+
31
+ // query layer + persistence
32
+ export { createStapelQueryClient } from "./query.js";
33
+ export type {
34
+ StapelQueryRuntime,
35
+ StapelQueryClientOptions,
36
+ PersistStorage,
37
+ } from "./query.js";
38
+
39
+ // i18n engine
40
+ export { createI18n, interpolate, I18nProvider, useI18n, useT } from "./i18n.js";
41
+ export type {
42
+ I18nEngine,
43
+ I18nDictionary,
44
+ LocaleLoader,
45
+ TranslateFn,
46
+ CreateI18nOptions,
47
+ } from "./i18n.js";
48
+
49
+ // analytics facade (analytics-standard §2)
50
+ export { createAnalytics } from "./analytics/createAnalytics.js";
51
+ export {
52
+ consoleProvider,
53
+ stapelCollectorProvider,
54
+ } from "./analytics/providers.js";
55
+ export type { StapelCollectorOptions } from "./analytics/providers.js";
56
+ export { trackFlowStep } from "./analytics/flow.js";
57
+ export type { FlowStepPhase } from "./analytics/flow.js";
58
+ export { AnalyticsContext, useAnalytics } from "./analytics/context.js";
59
+ export type {
60
+ Analytics,
61
+ AnalyticsEvent,
62
+ AnalyticsEventKind,
63
+ AnalyticsProvider,
64
+ AnalyticsOptions,
65
+ AnalyticsBatchOptions,
66
+ ConsentState,
67
+ PiiGuardMode,
68
+ } from "./analytics/types.js";
69
+
70
+ // breakpoints
71
+ export { useBreakpoint } from "./useBreakpoint.js";
72
+ export type { Breakpoint } from "@stapel/tokens";