@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.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/analytics/context.d.ts +9 -0
- package/dist/analytics/context.d.ts.map +1 -0
- package/dist/analytics/context.js +14 -0
- package/dist/analytics/context.js.map +1 -0
- package/dist/analytics/createAnalytics.d.ts +10 -0
- package/dist/analytics/createAnalytics.d.ts.map +1 -0
- package/dist/analytics/createAnalytics.js +273 -0
- package/dist/analytics/createAnalytics.js.map +1 -0
- package/dist/analytics/flow.d.ts +10 -0
- package/dist/analytics/flow.d.ts.map +1 -0
- package/dist/analytics/flow.js +10 -0
- package/dist/analytics/flow.js.map +1 -0
- package/dist/analytics/hash.d.ts +3 -0
- package/dist/analytics/hash.d.ts.map +1 -0
- package/dist/analytics/hash.js +12 -0
- package/dist/analytics/hash.js.map +1 -0
- package/dist/analytics/pii.d.ts +9 -0
- package/dist/analytics/pii.d.ts.map +1 -0
- package/dist/analytics/pii.js +52 -0
- package/dist/analytics/pii.js.map +1 -0
- package/dist/analytics/providers.d.ts +28 -0
- package/dist/analytics/providers.d.ts.map +1 -0
- package/dist/analytics/providers.js +82 -0
- package/dist/analytics/providers.js.map +1 -0
- package/dist/analytics/types.d.ts +94 -0
- package/dist/analytics/types.d.ts.map +1 -0
- package/dist/analytics/types.js +7 -0
- package/dist/analytics/types.js.map +1 -0
- package/dist/client.d.ts +49 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +135 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/i18n.d.ts +51 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +90 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/query.d.ts +42 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +95 -0
- package/dist/query.js.map +1 -0
- package/dist/storage.d.ts +16 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +65 -0
- package/dist/storage.js.map +1 -0
- package/dist/useBreakpoint.d.ts +8 -0
- package/dist/useBreakpoint.d.ts.map +1 -0
- package/dist/useBreakpoint.js +22 -0
- package/dist/useBreakpoint.js.map +1 -0
- package/dist/verification.d.ts +31 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +20 -0
- package/dist/verification.js.map +1 -0
- package/package.json +68 -0
- package/src/analytics/context.ts +20 -0
- package/src/analytics/createAnalytics.ts +310 -0
- package/src/analytics/flow.ts +19 -0
- package/src/analytics/hash.ts +16 -0
- package/src/analytics/pii.ts +66 -0
- package/src/analytics/providers.ts +108 -0
- package/src/analytics/types.ts +105 -0
- package/src/client.ts +206 -0
- package/src/config.tsx +62 -0
- package/src/errors.ts +70 -0
- package/src/i18n.tsx +147 -0
- package/src/index.ts +72 -0
- package/src/query.ts +151 -0
- package/src/storage.ts +76 -0
- package/src/useBreakpoint.ts +27 -0
- package/src/verification.ts +48 -0
- package/tsconfig.json +26 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stapel contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @stapel/core
|
|
2
|
+
|
|
3
|
+
The Stapel frontend runtime (L0, frontend-standard §1). Everything the
|
|
4
|
+
`@stapel/<module>-react` pairs build on:
|
|
5
|
+
|
|
6
|
+
- **`createStapelClient`** — typed fetch wrapper: base URL, auth token getter
|
|
7
|
+
+ refresh seam, parses the Stapel error envelope
|
|
8
|
+
`{localizable_error, error, params}` into `StapelApiError`
|
|
9
|
+
(`code` = the `localizable_error` i18n key, `params` for interpolation).
|
|
10
|
+
- **Verification-403 interception** — when a 403 body carries a
|
|
11
|
+
`verification` object (`challenge_id`/`scope`/`factors`), the configured
|
|
12
|
+
`onVerificationChallenge(challenge)` is invoked; on
|
|
13
|
+
`{ retry: true, token }` the original request is retried once with
|
|
14
|
+
`X-Verification-Token`. This is the seam `@stapel/auth-react`'s factor
|
|
15
|
+
machines plug into (the flagship cross-module flow, standard §2).
|
|
16
|
+
- **`StapelConfigProvider`** — provides the default client plus per-module
|
|
17
|
+
client overrides (client injection, the fork-resolution seam of §7.2).
|
|
18
|
+
- **Query layer** — `createStapelQueryClient()`: TanStack Query v5 client
|
|
19
|
+
with a persistence runtime: IndexedDB via `idb-keyval`, `localStorage`
|
|
20
|
+
fallback, in-memory last resort; **per-user namespace** via
|
|
21
|
+
`setPersistUser(userId)`; `purgePersistedCache()` for logout/GDPR;
|
|
22
|
+
cache-version buster (set it to your package version).
|
|
23
|
+
- **i18n engine** — key→string dictionaries, `{param}` interpolation,
|
|
24
|
+
`I18nProvider` / `useT` / `useI18n`, static bundles + async locale loader
|
|
25
|
+
seam (point it at `translate.resolve` of stapel-translate), missing keys
|
|
26
|
+
fall back to the key itself.
|
|
27
|
+
- **`useBreakpoint()`** — resolves the three `@stapel/tokens` breakpoints;
|
|
28
|
+
SSR-safe (`undefined` until mounted).
|
|
29
|
+
- **Analytics facade** — see the dedicated section below
|
|
30
|
+
(analytics-standard §2).
|
|
31
|
+
|
|
32
|
+
## Analytics facade
|
|
33
|
+
|
|
34
|
+
Per [analytics-standard](../../../docs/analytics-standard.md) §1–2 and
|
|
35
|
+
frontend-standard §4.7: packages and hosts talk to the facade only, never to
|
|
36
|
+
providers directly.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import {
|
|
40
|
+
createAnalytics,
|
|
41
|
+
consoleProvider,
|
|
42
|
+
stapelCollectorProvider,
|
|
43
|
+
trackFlowStep,
|
|
44
|
+
} from "@stapel/core";
|
|
45
|
+
|
|
46
|
+
const analytics = createAnalytics({
|
|
47
|
+
providers: {
|
|
48
|
+
stapel: stapelCollectorProvider({ client, writeKey: "wk_…" }),
|
|
49
|
+
console: consoleProvider(), // dev
|
|
50
|
+
},
|
|
51
|
+
registry: eventsJson.map((e) => e.name), // dev warning; hard gate = eslint
|
|
52
|
+
piiGuard: "strip", // email/phone-like prop values → "[redacted]"
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
analytics.track("cart.checkout", { total: 42 });
|
|
56
|
+
analytics.identify(user.id, { plan: "pro" }); // id is SHA-256-hashed first
|
|
57
|
+
analytics.page("pricing");
|
|
58
|
+
await analytics.setConsent("granted"); // "pending" buffers, "denied" drops
|
|
59
|
+
trackFlowStep(analytics, "onboarding", "otp", "completed"); // flow.<id>.<step>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Semantics: consent gate (buffer while `pending`, flush on `granted`, drop +
|
|
63
|
+
no-op on `denied`; consent persists), offline queue on the shared persist
|
|
64
|
+
storage (IndexedDB → localStorage → memory; survives instance recreation),
|
|
65
|
+
batched fan-out to all registered providers with per-provider delivery
|
|
66
|
+
tracking, exponential-backoff retries (drop with a warning after
|
|
67
|
+
`maxAttempts`), flush on interval / `maxSize` / explicit `flush()` /
|
|
68
|
+
`pagehide` + `visibilitychange: hidden` (the Stapel collector uses
|
|
69
|
+
`navigator.sendBeacon` for that final batch). `register`/`unregister` change
|
|
70
|
+
the fan-out at runtime — extra providers (Mixpanel, GA4, PostHog…) are tiny
|
|
71
|
+
`@stapel/analytics-<provider>` packages or app-layer classes. Wire React via
|
|
72
|
+
`<StapelConfigProvider analytics={analytics}>` and `useAnalytics()`.
|
|
73
|
+
|
|
74
|
+
## Quick start
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import {
|
|
78
|
+
createStapelClient,
|
|
79
|
+
createStapelQueryClient,
|
|
80
|
+
createI18n,
|
|
81
|
+
StapelConfigProvider,
|
|
82
|
+
I18nProvider,
|
|
83
|
+
} from "@stapel/core";
|
|
84
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
85
|
+
|
|
86
|
+
const client = createStapelClient({
|
|
87
|
+
baseUrl: "https://api.example.com",
|
|
88
|
+
getToken: () => auth.accessToken,
|
|
89
|
+
onAuthRefresh: () => auth.refresh(),
|
|
90
|
+
onVerificationChallenge: (challenge) => verificationUi.run(challenge),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const query = createStapelQueryClient({ cacheVersion: "0.1.0" });
|
|
94
|
+
const i18n = createI18n({
|
|
95
|
+
locale: "en",
|
|
96
|
+
loadLocale: (locale) => translateClient.resolve(locale), // stapel-translate
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
<StapelConfigProvider config={{ client }}>
|
|
100
|
+
<QueryClientProvider client={query.queryClient}>
|
|
101
|
+
<I18nProvider i18n={i18n}>{app}</I18nProvider>
|
|
102
|
+
</QueryClientProvider>
|
|
103
|
+
</StapelConfigProvider>;
|
|
104
|
+
|
|
105
|
+
// on login: await query.setPersistUser(user.id);
|
|
106
|
+
// on logout: await query.purgePersistedCache();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Notes
|
|
110
|
+
|
|
111
|
+
- Peer deps: `react >= 19`, `@tanstack/react-query ^5`. Only runtime dep:
|
|
112
|
+
`idb-keyval` (+ `@stapel/tokens`).
|
|
113
|
+
- Standalone-buildable; the npm tarball ships `src/` (frontend-standard §7).
|
|
114
|
+
- TODO(frontend-standard §4.5): precompile with React Compiler at publish so
|
|
115
|
+
consumers outside the Stapel toolchain get baked-in memoization. The package
|
|
116
|
+
is currently hooks-only (no JSX-heavy render paths), so the compiler yields
|
|
117
|
+
nothing yet; wire it into the publish pipeline together with the first
|
|
118
|
+
headless components.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Context } from "react";
|
|
2
|
+
import type { Analytics } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Provided by `<StapelConfigProvider analytics={...}>` (or directly via
|
|
5
|
+
* `AnalyticsContext.Provider`).
|
|
6
|
+
*/
|
|
7
|
+
export declare const AnalyticsContext: Context<Analytics | null>;
|
|
8
|
+
export declare function useAnalytics(): Analytics;
|
|
9
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/analytics/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AACrC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,OAAO,CAAC,SAAS,GAAG,IAAI,CAChB,CAAC;AAExC,wBAAgB,YAAY,IAAI,SAAS,CAQxC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Provided by `<StapelConfigProvider analytics={...}>` (or directly via
|
|
4
|
+
* `AnalyticsContext.Provider`).
|
|
5
|
+
*/
|
|
6
|
+
export const AnalyticsContext = createContext(null);
|
|
7
|
+
export function useAnalytics() {
|
|
8
|
+
const analytics = useContext(AnalyticsContext);
|
|
9
|
+
if (analytics === null) {
|
|
10
|
+
throw new Error("useAnalytics requires an analytics instance — pass it to <StapelConfigProvider analytics={...}>");
|
|
11
|
+
}
|
|
12
|
+
return analytics;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/analytics/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAIlD;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAC3B,aAAa,CAAmB,IAAI,CAAC,CAAC;AAExC,MAAM,UAAU,YAAY;IAC1B,MAAM,SAAS,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC/C,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CACb,iGAAiG,CAClG,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Analytics, AnalyticsOptions } from "./types.js";
|
|
2
|
+
/** Exponential backoff delay for a batch delivery attempt (1-based). */
|
|
3
|
+
export declare function backoffDelay(attempt: number, baseMs: number): number;
|
|
4
|
+
/**
|
|
5
|
+
* The analytics facade (analytics-standard §2): fan-out to N providers,
|
|
6
|
+
* consent gate, offline queue on the core persist layer, batched delivery
|
|
7
|
+
* with retry/backoff, PII guard, hashed identify.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createAnalytics(options?: AnalyticsOptions): Analytics;
|
|
10
|
+
//# sourceMappingURL=createAnalytics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createAnalytics.d.ts","sourceRoot":"","sources":["../../src/analytics/createAnalytics.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EAGT,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AAEpB,wEAAwE;AACxE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpE;AAyBD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS,CAuQzE"}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { defaultPersistStorage } from "../storage.js";
|
|
2
|
+
import { guardPii } from "./pii.js";
|
|
3
|
+
import { sha256Hex } from "./hash.js";
|
|
4
|
+
/** Exponential backoff delay for a batch delivery attempt (1-based). */
|
|
5
|
+
export function backoffDelay(attempt, baseMs) {
|
|
6
|
+
return Math.min(30_000, baseMs * 2 ** Math.max(0, attempt - 1));
|
|
7
|
+
}
|
|
8
|
+
async function deliverToProvider(provider, events) {
|
|
9
|
+
for (const event of events) {
|
|
10
|
+
if (event.kind === "identify" && provider.identify) {
|
|
11
|
+
await provider.identify(event.userHash ?? "", event.props);
|
|
12
|
+
}
|
|
13
|
+
else if (event.kind === "page" && provider.page) {
|
|
14
|
+
await provider.page(event.name, event.props);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
await provider.track(event);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
await provider.flush?.();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The analytics facade (analytics-standard §2): fan-out to N providers,
|
|
24
|
+
* consent gate, offline queue on the core persist layer, batched delivery
|
|
25
|
+
* with retry/backoff, PII guard, hashed identify.
|
|
26
|
+
*/
|
|
27
|
+
export function createAnalytics(options = {}) {
|
|
28
|
+
const providers = new Map(Object.entries(options.providers ?? {}));
|
|
29
|
+
const registry = options.registry !== undefined ? new Set(options.registry) : null;
|
|
30
|
+
const piiMode = options.piiGuard ?? "strip";
|
|
31
|
+
const persistKey = options.persistKey ?? "stapel-analytics";
|
|
32
|
+
const storage = options.storage ?? defaultPersistStorage();
|
|
33
|
+
const maxSize = options.batch?.maxSize ?? 20;
|
|
34
|
+
const flushIntervalMs = options.batch?.flushIntervalMs ?? 10_000;
|
|
35
|
+
const maxAttempts = options.batch?.maxAttempts ?? 5;
|
|
36
|
+
const backoffBaseMs = options.batch?.backoffBaseMs ?? 500;
|
|
37
|
+
const eventsKey = `${persistKey}:events`;
|
|
38
|
+
const consentKey = `${persistKey}:consent`;
|
|
39
|
+
let consent = options.consent ?? "pending";
|
|
40
|
+
let queue = [];
|
|
41
|
+
let batch = null;
|
|
42
|
+
let flushing = false;
|
|
43
|
+
let retryTimer = null;
|
|
44
|
+
let seq = 0;
|
|
45
|
+
const warnedPii = new Set();
|
|
46
|
+
const warnedRegistry = new Set();
|
|
47
|
+
/** In-flight async work (identify hashing, persist writes) awaited by flush. */
|
|
48
|
+
let pendingOps = [];
|
|
49
|
+
/** Serializes identify hashing so enqueue order matches call order. */
|
|
50
|
+
let identifyChain = Promise.resolve();
|
|
51
|
+
const ready = (async () => {
|
|
52
|
+
try {
|
|
53
|
+
const storedConsent = await storage.get(consentKey);
|
|
54
|
+
if (storedConsent === "granted" ||
|
|
55
|
+
storedConsent === "denied" ||
|
|
56
|
+
storedConsent === "pending") {
|
|
57
|
+
consent = storedConsent;
|
|
58
|
+
}
|
|
59
|
+
const storedEvents = await storage.get(eventsKey);
|
|
60
|
+
if (Array.isArray(storedEvents)) {
|
|
61
|
+
// Restored events precede anything captured before init finished.
|
|
62
|
+
queue = [...storedEvents, ...queue];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Storage unavailable — degrade to in-memory only.
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
async function persistQueue() {
|
|
70
|
+
try {
|
|
71
|
+
if (consent === "denied") {
|
|
72
|
+
await storage.del(eventsKey);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const pending = batch ? [...batch.events, ...queue] : queue;
|
|
76
|
+
await storage.set(eventsKey, pending);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Best-effort.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function schedulePersist() {
|
|
83
|
+
pendingOps.push(ready.then(persistQueue));
|
|
84
|
+
}
|
|
85
|
+
function clearRetryTimer() {
|
|
86
|
+
if (retryTimer !== null) {
|
|
87
|
+
clearTimeout(retryTimer);
|
|
88
|
+
retryTimer = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function scheduleRetry(delayMs) {
|
|
92
|
+
if (retryTimer !== null)
|
|
93
|
+
return;
|
|
94
|
+
retryTimer = setTimeout(() => {
|
|
95
|
+
retryTimer = null;
|
|
96
|
+
void flush();
|
|
97
|
+
}, delayMs);
|
|
98
|
+
retryTimer.unref?.();
|
|
99
|
+
}
|
|
100
|
+
function enqueue(kind, name, props, userHash) {
|
|
101
|
+
seq += 1;
|
|
102
|
+
const event = {
|
|
103
|
+
id: `${String(Date.now())}-${String(seq)}`,
|
|
104
|
+
kind,
|
|
105
|
+
name,
|
|
106
|
+
props,
|
|
107
|
+
...(userHash !== undefined ? { userHash } : {}),
|
|
108
|
+
ts: Date.now(),
|
|
109
|
+
};
|
|
110
|
+
queue.push(event);
|
|
111
|
+
schedulePersist();
|
|
112
|
+
if (consent === "granted" && queue.length >= maxSize) {
|
|
113
|
+
void flush();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** One delivery attempt for the current batch. True = batch settled. */
|
|
117
|
+
async function attemptBatch() {
|
|
118
|
+
const current = batch;
|
|
119
|
+
if (current === null)
|
|
120
|
+
return true;
|
|
121
|
+
current.attempts += 1;
|
|
122
|
+
await Promise.all([...providers.entries()].map(async ([name, provider]) => {
|
|
123
|
+
if (current.delivered.has(name))
|
|
124
|
+
return;
|
|
125
|
+
try {
|
|
126
|
+
await deliverToProvider(provider, current.events);
|
|
127
|
+
current.delivered.add(name);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Undelivered for this provider; batch will be retried.
|
|
131
|
+
}
|
|
132
|
+
}));
|
|
133
|
+
const undelivered = [...providers.keys()].filter((name) => !current.delivered.has(name));
|
|
134
|
+
if (undelivered.length === 0) {
|
|
135
|
+
batch = null;
|
|
136
|
+
clearRetryTimer();
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (current.attempts >= maxAttempts) {
|
|
140
|
+
console.warn(`[stapel analytics] dropping a batch of ${String(current.events.length)} event(s) ` +
|
|
141
|
+
`after ${String(current.attempts)} attempts; undelivered to: ${undelivered.join(", ")}`);
|
|
142
|
+
batch = null;
|
|
143
|
+
clearRetryTimer();
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
scheduleRetry(backoffDelay(current.attempts, backoffBaseMs));
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
async function flush() {
|
|
150
|
+
await ready;
|
|
151
|
+
const ops = pendingOps;
|
|
152
|
+
pendingOps = [];
|
|
153
|
+
await Promise.all(ops);
|
|
154
|
+
if (flushing)
|
|
155
|
+
return;
|
|
156
|
+
flushing = true;
|
|
157
|
+
try {
|
|
158
|
+
if (consent !== "granted")
|
|
159
|
+
return;
|
|
160
|
+
for (;;) {
|
|
161
|
+
if (batch === null) {
|
|
162
|
+
if (queue.length === 0 || providers.size === 0)
|
|
163
|
+
break;
|
|
164
|
+
batch = {
|
|
165
|
+
events: queue.splice(0, maxSize),
|
|
166
|
+
attempts: 0,
|
|
167
|
+
delivered: new Set(),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const settled = await attemptBatch();
|
|
171
|
+
if (!settled)
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
flushing = false;
|
|
177
|
+
await persistQueue();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function setConsent(state) {
|
|
181
|
+
await ready;
|
|
182
|
+
consent = state;
|
|
183
|
+
try {
|
|
184
|
+
await storage.set(consentKey, state);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Best-effort.
|
|
188
|
+
}
|
|
189
|
+
if (state === "denied") {
|
|
190
|
+
queue = [];
|
|
191
|
+
batch = null;
|
|
192
|
+
clearRetryTimer();
|
|
193
|
+
try {
|
|
194
|
+
await storage.del(eventsKey);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Best-effort.
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (state === "granted") {
|
|
202
|
+
await flush();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function track(event, props) {
|
|
206
|
+
if (consent === "denied")
|
|
207
|
+
return;
|
|
208
|
+
if (registry && !registry.has(event) && !warnedRegistry.has(event)) {
|
|
209
|
+
warnedRegistry.add(event);
|
|
210
|
+
console.warn(`[stapel analytics] event "${event}" is not in the registry ` +
|
|
211
|
+
"(analytics-standard §1.1) — delivered anyway; declare it in events.json.");
|
|
212
|
+
}
|
|
213
|
+
enqueue("track", event, guardPii(event, props ?? {}, piiMode, warnedPii));
|
|
214
|
+
}
|
|
215
|
+
function page(name, props) {
|
|
216
|
+
if (consent === "denied")
|
|
217
|
+
return;
|
|
218
|
+
enqueue("page", name, guardPii(name, props ?? {}, piiMode, warnedPii));
|
|
219
|
+
}
|
|
220
|
+
function identify(userId, traits) {
|
|
221
|
+
if (consent === "denied")
|
|
222
|
+
return;
|
|
223
|
+
const guarded = guardPii("identify", traits ?? {}, piiMode, warnedPii);
|
|
224
|
+
identifyChain = identifyChain
|
|
225
|
+
.then(() => sha256Hex(userId))
|
|
226
|
+
.then((userHash) => {
|
|
227
|
+
enqueue("identify", "identify", guarded, userHash);
|
|
228
|
+
});
|
|
229
|
+
pendingOps.push(identifyChain);
|
|
230
|
+
}
|
|
231
|
+
function finalFlush() {
|
|
232
|
+
// Best-effort teardown flush; batching providers use sendBeacon here.
|
|
233
|
+
void flush();
|
|
234
|
+
for (const provider of providers.values()) {
|
|
235
|
+
try {
|
|
236
|
+
void provider.flush?.();
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// Never break page teardown.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (typeof window !== "undefined") {
|
|
244
|
+
window.addEventListener("pagehide", finalFlush);
|
|
245
|
+
}
|
|
246
|
+
if (typeof document !== "undefined") {
|
|
247
|
+
document.addEventListener("visibilitychange", () => {
|
|
248
|
+
if (document.visibilityState === "hidden")
|
|
249
|
+
finalFlush();
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const interval = setInterval(() => {
|
|
253
|
+
if (consent === "granted" && (queue.length > 0 || batch !== null)) {
|
|
254
|
+
void flush();
|
|
255
|
+
}
|
|
256
|
+
}, flushIntervalMs);
|
|
257
|
+
interval.unref?.();
|
|
258
|
+
return {
|
|
259
|
+
track,
|
|
260
|
+
identify,
|
|
261
|
+
page,
|
|
262
|
+
flush,
|
|
263
|
+
setConsent,
|
|
264
|
+
getConsent: () => consent,
|
|
265
|
+
register: (name, provider) => {
|
|
266
|
+
providers.set(name, provider);
|
|
267
|
+
},
|
|
268
|
+
unregister: (name) => {
|
|
269
|
+
providers.delete(name);
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=createAnalytics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createAnalytics.js","sourceRoot":"","sources":["../../src/analytics/createAnalytics.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Analytics } from "./types.js";
|
|
2
|
+
export type FlowStepPhase = "started" | "completed" | "failed";
|
|
3
|
+
/**
|
|
4
|
+
* Auto-instrumentation seam for flow machines (analytics-standard §1.2):
|
|
5
|
+
* emits `flow.<flowId>.<stepId>` with `{phase, ...props}`. Funnel = flow —
|
|
6
|
+
* the `@stapel/<module>-react` machines call this on every transition, so
|
|
7
|
+
* funnels exist without hand-written instrumentation.
|
|
8
|
+
*/
|
|
9
|
+
export declare function trackFlowStep(analytics: Analytics, flowId: string, stepId: string, phase: FlowStepPhase, props?: Record<string, unknown>): void;
|
|
10
|
+
//# sourceMappingURL=flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow.d.ts","sourceRoot":"","sources":["../../src/analytics/flow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE/D;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,aAAa,EACpB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,IAAI,CAEN"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-instrumentation seam for flow machines (analytics-standard §1.2):
|
|
3
|
+
* emits `flow.<flowId>.<stepId>` with `{phase, ...props}`. Funnel = flow —
|
|
4
|
+
* the `@stapel/<module>-react` machines call this on every transition, so
|
|
5
|
+
* funnels exist without hand-written instrumentation.
|
|
6
|
+
*/
|
|
7
|
+
export function trackFlowStep(analytics, flowId, stepId, phase, props) {
|
|
8
|
+
analytics.track(`flow.${flowId}.${stepId}`, { phase, ...props });
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow.js","sourceRoot":"","sources":["../../src/analytics/flow.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,SAAoB,EACpB,MAAc,EACd,MAAc,EACd,KAAoB,EACpB,KAA+B;IAE/B,SAAS,CAAC,KAAK,CAAC,QAAQ,MAAM,IAAI,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;AACnE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../src/analytics/hash.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc9D"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** SHA-256 hex via WebCrypto — user ids are hashed before any provider sees them. */
|
|
2
|
+
export async function sha256Hex(input) {
|
|
3
|
+
const subtle = globalThis.crypto?.subtle;
|
|
4
|
+
if (!subtle) {
|
|
5
|
+
throw new Error("[stapel analytics] crypto.subtle is unavailable; cannot hash user ids");
|
|
6
|
+
}
|
|
7
|
+
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
8
|
+
return [...new Uint8Array(digest)]
|
|
9
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
10
|
+
.join("");
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=hash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/analytics/hash.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,KAAa;IAC3C,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC;IACzC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,uEAAuE,CACxE,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAChC,SAAS,EACT,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAChC,CAAC;IACF,OAAO,CAAC,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;SAC/B,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;SACjD,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PiiGuardMode } from "./types.js";
|
|
2
|
+
export declare function looksLikePii(value: string): boolean;
|
|
3
|
+
export declare const PII_REDACTED = "[redacted]";
|
|
4
|
+
/**
|
|
5
|
+
* Guard a props/traits object. Warns once per event name (per facade
|
|
6
|
+
* instance) via the caller-owned `warned` set.
|
|
7
|
+
*/
|
|
8
|
+
export declare function guardPii(eventName: string, props: Record<string, unknown>, mode: PiiGuardMode, warned: Set<string>): Record<string, unknown>;
|
|
9
|
+
//# sourceMappingURL=pii.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pii.d.ts","sourceRoot":"","sources":["../../src/analytics/pii.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAY/C,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAKnD;AAED,eAAO,MAAM,YAAY,eAAe,CAAC;AAwBzC;;;GAGG;AACH,wBAAgB,QAAQ,CACtB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,GAClB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAazB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII guard heuristics (analytics-standard §1.4): prop VALUES that look
|
|
3
|
+
* like emails or phone numbers are redacted ("strip"), kept with a warning
|
|
4
|
+
* ("warn"), or passed through ("off"). Applies to track/page props and
|
|
5
|
+
* identify traits. Keys are not judged — only values.
|
|
6
|
+
*/
|
|
7
|
+
const EMAIL_RE = /[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}/;
|
|
8
|
+
const PHONE_SHAPE_RE = /^\+?[\d\s\-().]{7,}$/;
|
|
9
|
+
export function looksLikePii(value) {
|
|
10
|
+
if (EMAIL_RE.test(value))
|
|
11
|
+
return true;
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
if (!PHONE_SHAPE_RE.test(trimmed))
|
|
14
|
+
return false;
|
|
15
|
+
return trimmed.replace(/\D/g, "").length >= 7;
|
|
16
|
+
}
|
|
17
|
+
export const PII_REDACTED = "[redacted]";
|
|
18
|
+
function sanitizeValue(value, mode, hit) {
|
|
19
|
+
if (typeof value === "string" && looksLikePii(value)) {
|
|
20
|
+
hit.found = true;
|
|
21
|
+
return mode === "strip" ? PII_REDACTED : value;
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
return value.map((item) => sanitizeValue(item, mode, hit));
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === "object" && value !== null) {
|
|
27
|
+
const result = {};
|
|
28
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
29
|
+
result[key] = sanitizeValue(nested, mode, hit);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Guard a props/traits object. Warns once per event name (per facade
|
|
37
|
+
* instance) via the caller-owned `warned` set.
|
|
38
|
+
*/
|
|
39
|
+
export function guardPii(eventName, props, mode, warned) {
|
|
40
|
+
if (mode === "off")
|
|
41
|
+
return props;
|
|
42
|
+
const hit = { found: false };
|
|
43
|
+
const guarded = sanitizeValue(props, mode, hit);
|
|
44
|
+
if (hit.found && !warned.has(eventName)) {
|
|
45
|
+
warned.add(eventName);
|
|
46
|
+
console.warn(`[stapel analytics] PII-like value in props of "${eventName}" — ` +
|
|
47
|
+
(mode === "strip" ? "redacted" : "kept (piiGuard: warn)") +
|
|
48
|
+
". PII in analytics props is banned (analytics-standard §1.4).");
|
|
49
|
+
}
|
|
50
|
+
return guarded;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=pii.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pii.js","sourceRoot":"","sources":["../../src/analytics/pii.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,MAAM,QAAQ,GAAG,+BAA+B,CAAC;AACjD,MAAM,cAAc,GAAG,sBAAsB,CAAC;AAE9C,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IAChD,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,YAAY,CAAC;AAEzC,SAAS,aAAa,CACpB,KAAc,EACd,IAAkB,EAClB,GAAuB;IAEvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC;QACjB,OAAO,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC;IACjD,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAC7D,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAChD,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAClD,MAAM,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CACtB,SAAiB,EACjB,KAA8B,EAC9B,IAAkB,EAClB,MAAmB;IAEnB,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IACjC,MAAM,GAAG,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAA4B,CAAC;IAC3E,IAAI,GAAG,CAAC,KAAK,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QACxC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACtB,OAAO,CAAC,IAAI,CACV,kDAAkD,SAAS,MAAM;YAC/D,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,uBAAuB,CAAC;YACzD,+DAA+D,CAClE,CAAC;IACJ,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StapelClient } from "../client.js";
|
|
2
|
+
import type { AnalyticsProvider } from "./types.js";
|
|
3
|
+
/** Dev provider: logs every event via console.debug. Never throws. */
|
|
4
|
+
export declare function consoleProvider(): AnalyticsProvider;
|
|
5
|
+
export interface StapelCollectorOptions {
|
|
6
|
+
/** Collector origin, e.g. `https://api.example.com`. */
|
|
7
|
+
readonly baseUrl?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Alternatively reuse a `StapelClient` (its base URL, auth headers and
|
|
10
|
+
* error envelope handling).
|
|
11
|
+
*/
|
|
12
|
+
readonly client?: StapelClient;
|
|
13
|
+
/** Source write key (analytics-standard §3); sent in the batch body. */
|
|
14
|
+
readonly writeKey?: string;
|
|
15
|
+
/** Injectable fetch (tests). */
|
|
16
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Built-in provider for the stapel-analytics ingest endpoint: buffers
|
|
20
|
+
* events handed over by the facade and POSTs `{events: [...]}` to
|
|
21
|
+
* `{base}/analytics/api/events` on flush (one request per facade batch).
|
|
22
|
+
* During page teardown (document hidden) it prefers `navigator.sendBeacon`
|
|
23
|
+
* so the final batch survives navigation; otherwise fetch with keepalive.
|
|
24
|
+
* On a failed send the buffer is surrendered back to the facade's retry
|
|
25
|
+
* (the facade re-delivers the batch, repopulating the buffer).
|
|
26
|
+
*/
|
|
27
|
+
export declare function stapelCollectorProvider(options: StapelCollectorOptions): AnalyticsProvider;
|
|
28
|
+
//# sourceMappingURL=providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../../src/analytics/providers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAkB,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpE,sEAAsE;AACtE,wBAAgB,eAAe,IAAI,iBAAiB,CAmBnD;AAED,MAAM,WAAW,sBAAsB;IACrC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,gCAAgC;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAC1C;AAID;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,sBAAsB,GAC9B,iBAAiB,CAuDnB"}
|