@mirafive/protocol 0.0.0-stage → 2.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cloo GmbH
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 CHANGED
@@ -1,3 +1,66 @@
1
- # Temporary Holding Version
1
+ # @mirafive/protocol
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The MIRA FIVE wire format and the helpers every MIRA FIVE SDK shares: event and batch types,
4
+ limits, reserved names, `eventIdFor`, UUIDv7, property cleaning and retry rules. No
5
+ dependencies; runs in browsers, Node, Bun, Deno, workers and Convex.
6
+
7
+ The MIRA FIVE ingest checks its own schema against this package on every build, so what
8
+ `validateBatch` accepts is what the ingest accepts.
9
+
10
+ You rarely install it yourself: [`@mirafive/web`](https://github.com/mirafive/sdk-web) and
11
+ [`@mirafive/node`](https://github.com/mirafive/sdk-node) depend on it. Use it to build your own
12
+ integration, to validate batches, or to port MIRA FIVE to another language.
13
+
14
+ ## Contents
15
+
16
+ | Export | What it is |
17
+ | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
18
+ | `WireEvent`, `WireBatch`, `BatchResult`, `EventContext`, `Revenue`, `Consent` | The types of what `POST /v1/batch` accepts and answers. They are checked against the ingest's own schema on every build, so they cannot drift. |
19
+ | `LIMITS`, `reservedEvents`, `externalIdTypes`, `revenueKinds`, `billingIntervals`, `touchpointKinds`, `touchpointChannels` | The rules, as values. |
20
+ | `eventIdFor(key)` | A stable event id from your own key: UUIDv8 from SHA-256. The same key gives the same id in every MIRA FIVE SDK and language. |
21
+ | `uuidv7()` | Time-ordered event ids. |
22
+ | `cleanProperties(value)`, `cleanExternalIds(value)` | Make arbitrary values fit the wire: flatten objects to dot keys, apply the limits. |
23
+ | `retryable(status)`, `backoffMs(attempt, atLeastMs)`, `retryAfterMs(header)` | The retry rules every SDK follows. |
24
+ | `keyKind(key)` | `"public"` (`mira_pk_…`), `"secret"` (`mira_sk_…`) or `"unknown"`. |
25
+ | `validateBatch(batch)`, `validateEvent(event)` | Every problem the ingest would reject, as `{ path, message }`; an empty list means accepted. No dependencies. |
26
+ | `mirafive-validate` (`npx @mirafive/protocol batches.json`) | The same check on the command line, for SDKs in other languages and CI: one batch or a list, from a file or standard input; exits 1 on a problem. |
27
+ | `@mirafive/protocol/batch.schema.json` | JSON Schema (2020-12) of a batch, generated from the ingest's schema. |
28
+ | `@mirafive/protocol/fixtures/event-ids.json` | `eventIdFor` test vectors for SDKs in other languages. |
29
+ | `@mirafive/protocol/fixtures/batches/{valid,invalid}/*.json` | Batches the ingest accepts and rejects, for SDK test suites. |
30
+
31
+ ## The wire in one request
32
+
33
+ ```sh
34
+ curl https://events.mirafive.io/v1/batch \
35
+ -H "Authorization: Bearer $MIRAFIVE_SECRET_KEY" \
36
+ -H "Content-Type: application/json" \
37
+ -d '{"events":[{"event_id":"0192d4a8-7b1c-7e8a-9c1d-2b3e4f5a6b7c","type":"track","event":"report_exported","user_id":"user_42","consent":"not_applicable"}]}'
38
+ ```
39
+
40
+ The answer is `{ "accepted": 1, "duplicates": 0, "rejected": [] }`. Events are checked one by
41
+ one, so a bad event never sinks the batch; resend a batch with the same `event_id`s and it counts
42
+ once.
43
+
44
+ ## For AI coding assistants
45
+
46
+ Rules for integrating MIRA FIVE correctly. They hold for every MIRA FIVE SDK.
47
+
48
+ - **Keys:** the public key (`mira_pk_…`) goes in browser and app code. The secret key (`mira_sk_…`)
49
+ goes only into the server's environment (`MIRAFIVE_SECRET_KEY`), never into client code, a
50
+ `NEXT_PUBLIC_`/`VITE_`/`PUBLIC_` variable or a repository. The SDKs refuse the wrong key.
51
+ - **Money** is sent only from the server: integer minor units plus an ISO 4217 currency
52
+ (`amount: 1999, currency: "EUR"` is 19.99 EUR), never floats.
53
+ - **Count once:** give money an `entryId` from the payment provider (`stripe:in_123`), so the
54
+ Stripe connector and retries do not double it. Give business events a stable id:
55
+ `eventId: await eventIdFor("order:123:paid")`. The same key gives the same id in every SDK.
56
+ - **Personal data** never goes into event names or properties. Use your own user id (not an email)
57
+ for `identify`; put email, name and phone in its traits, which MIRA FIVE encrypts at rest.
58
+ - **Consent:** keep `consent: "auto"`; it follows the site's consent banner (Usercentrics,
59
+ Cookiebot, consentmanager, CCM19, Borlabs, any TCF 2.2 CMP, Google Consent Mode). Call
60
+ `consent()` only for a custom banner. Do not gate the SDK behind consent yourself: without
61
+ consent it is cookieless and still counts.
62
+ - **Never block on analytics:** event methods return nothing, never throw, and batch on their
63
+ own. Do not `await` or `try` them. Call `flush()`/`shutdown()` only at the end of a script or a
64
+ serverless function.
65
+ - **Server events and visitors:** pass the browser's `mira.anonymousId` to your server and set it
66
+ as `anonymousId` on server events, so a purchase joins the visit and its source.
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync } from "node:fs";
5
+
6
+ // src/wire.ts
7
+ var LIMITS = {
8
+ eventsPerBatch: 100,
9
+ nameLength: 120,
10
+ propertyKeys: 64,
11
+ propertyKeyLength: 100,
12
+ stringLength: 2048,
13
+ arrayLength: 50,
14
+ urlLength: 4096,
15
+ idLength: 200,
16
+ maxClientAgeDays: 7
17
+ };
18
+ var consentStates = ["granted", "denied", "unknown"];
19
+ var externalIdTypes = [
20
+ "stripe_customer",
21
+ "revenuecat_app_user",
22
+ "shopify_customer",
23
+ "shopware_customer"
24
+ ];
25
+ var revenueKinds = [
26
+ "charge",
27
+ "subscription_payment",
28
+ "refund",
29
+ "dispute",
30
+ "dispute_won"
31
+ ];
32
+ var billingIntervals = ["day", "week", "month", "year"];
33
+ var touchpointKinds = ["survey", "offline", "code", "custom"];
34
+ var touchpointChannels = [
35
+ "paid_search",
36
+ "paid_social",
37
+ "paid_other",
38
+ "organic_search",
39
+ "organic_social",
40
+ "ai_assistant",
41
+ "email",
42
+ "affiliate",
43
+ "display",
44
+ "video",
45
+ "qr",
46
+ "app_store",
47
+ "referral",
48
+ "direct"
49
+ ];
50
+
51
+ // src/validate.ts
52
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
53
+ var join = (path, key) => path ? `${path}.${key}` : String(key);
54
+ var UUID = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;
55
+ var DATETIME = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-])(\d{2}):(\d{2}))$/;
56
+ function isDateTime(value) {
57
+ const match = DATETIME.exec(value);
58
+ if (!match)
59
+ return false;
60
+ const [, year, month, day, hour, minute, second, , offsetHour, offsetMinute] = match;
61
+ const days = new Date(Date.UTC(Number(year), Number(month), 0)).getUTCDate();
62
+ return Number(month) >= 1 && Number(month) <= 12 && Number(day) >= 1 && Number(day) <= days && Number(hour) <= 23 && Number(minute) <= 59 && Number(second) <= 59 && (offsetHour === undefined || Number(offsetHour) <= 23 && Number(offsetMinute) <= 59);
63
+ }
64
+ var string = (options) => (value, path, issues) => {
65
+ if (typeof value !== "string") {
66
+ issues.push({ path, message: "expected a string" });
67
+ return;
68
+ }
69
+ const text = options.trim ? value.trim() : value;
70
+ if (options.min !== undefined && text.length < options.min) {
71
+ issues.push({ path, message: `at least ${options.min} character(s)` });
72
+ } else if (text.length > options.max) {
73
+ issues.push({ path, message: `at most ${options.max} characters` });
74
+ }
75
+ if (options.pattern && !options.pattern.test(text)) {
76
+ issues.push({ path, message: options.hint ?? "invalid format" });
77
+ }
78
+ };
79
+ var dateTime = (value, path, issues) => {
80
+ if (typeof value !== "string" || !isDateTime(value)) {
81
+ issues.push({ path, message: "expected an ISO 8601 date and time with Z or an offset" });
82
+ }
83
+ };
84
+ var oneOf = (values) => (value, path, issues) => {
85
+ if (typeof value !== "string" || !values.includes(value)) {
86
+ issues.push({ path, message: `expected one of ${values.join(", ")}` });
87
+ }
88
+ };
89
+ var integer = (min, max) => (value, path, issues) => {
90
+ if (typeof value !== "number" || !Number.isInteger(value)) {
91
+ {
92
+ issues.push({ path, message: "expected an integer" });
93
+ return;
94
+ }
95
+ }
96
+ if (value < min || value > max)
97
+ issues.push({ path, message: `expected ${min} to ${max}` });
98
+ };
99
+ var number = (options) => (value, path, issues) => {
100
+ if (typeof value !== "number" || !Number.isFinite(value)) {
101
+ {
102
+ issues.push({ path, message: "expected a number" });
103
+ return;
104
+ }
105
+ }
106
+ if (options.int && !Number.isInteger(value))
107
+ issues.push({ path, message: "expected an integer" });
108
+ if (options.positive && value <= 0)
109
+ issues.push({ path, message: "expected a positive number" });
110
+ if (options.min !== undefined && value < options.min) {
111
+ issues.push({ path, message: `at least ${options.min}` });
112
+ }
113
+ if (options.max !== undefined && value > options.max) {
114
+ issues.push({ path, message: `at most ${options.max}` });
115
+ }
116
+ };
117
+ var object = (fields, required = []) => (value, path, issues) => {
118
+ if (!isObject(value)) {
119
+ issues.push({ path, message: "expected an object" });
120
+ return;
121
+ }
122
+ for (const [key, check] of Object.entries(fields)) {
123
+ if (value[key] === undefined) {
124
+ if (required.includes(key))
125
+ issues.push({ path: join(path, key), message: "required" });
126
+ continue;
127
+ }
128
+ check(value[key], join(path, key), issues);
129
+ }
130
+ };
131
+ var scalar = (value) => typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.length <= LIMITS.stringLength;
132
+ var properties = (value, path, issues) => {
133
+ if (!isObject(value)) {
134
+ issues.push({ path, message: "expected an object" });
135
+ return;
136
+ }
137
+ const keys = Object.keys(value);
138
+ for (const key of keys) {
139
+ const at = join(path, key);
140
+ if (key.length < 1 || key.length > LIMITS.propertyKeyLength) {
141
+ issues.push({ path: at, message: `keys have 1 to ${LIMITS.propertyKeyLength} characters` });
142
+ }
143
+ const item = value[key];
144
+ if (item === null || scalar(item))
145
+ continue;
146
+ if (Array.isArray(item)) {
147
+ if (item.length > LIMITS.arrayLength) {
148
+ issues.push({ path: at, message: `at most ${LIMITS.arrayLength} items` });
149
+ }
150
+ item.forEach((entry, index) => {
151
+ if (!scalar(entry)) {
152
+ issues.push({ path: join(at, index), message: "expected a string, number or boolean" });
153
+ }
154
+ });
155
+ continue;
156
+ }
157
+ issues.push({
158
+ path: at,
159
+ message: typeof item === "string" ? `at most ${LIMITS.stringLength} characters` : "expected a string, number, boolean, null or a list of them (nested objects are flattened by the SDKs)"
160
+ });
161
+ }
162
+ if (keys.length > LIMITS.propertyKeys) {
163
+ issues.push({ path, message: `at most ${LIMITS.propertyKeys} properties per event` });
164
+ }
165
+ };
166
+ var built;
167
+ function rules() {
168
+ if (built)
169
+ return built;
170
+ const short = string({ max: LIMITS.stringLength });
171
+ const url = string({ max: LIMITS.urlLength });
172
+ const size = integer(0, 20000);
173
+ const context = object({
174
+ page: object({ url, referrer: url, title: short }),
175
+ app: object({ name: short, version: short, build: short, namespace: short }),
176
+ device: object({
177
+ type: oneOf(["desktop", "mobile", "tablet", "tv", "other"]),
178
+ manufacturer: short,
179
+ model: short
180
+ }),
181
+ os: object({ name: short, version: short }),
182
+ screen: object({ width: size, height: size, density: number({ positive: true, max: 10 }) }),
183
+ viewport: object({ width: size, height: size }),
184
+ locale: string({ max: 35 }),
185
+ languages: (value, path, issues) => {
186
+ if (!Array.isArray(value)) {
187
+ issues.push({ path, message: "expected a list" });
188
+ return;
189
+ }
190
+ if (value.length > 10)
191
+ issues.push({ path, message: "at most 10 languages" });
192
+ value.forEach((entry, index) => string({ max: 35 })(entry, join(path, index), issues));
193
+ },
194
+ timezone: string({ max: 64 }),
195
+ location: object({
196
+ latitude: number({ min: -90, max: 90 }),
197
+ longitude: number({ min: -180, max: 180 })
198
+ }, ["latitude", "longitude"]),
199
+ library: object({ name: string({ max: 50 }), version: string({ max: 30 }) }, [
200
+ "name",
201
+ "version"
202
+ ]),
203
+ ip: string({ max: 45 }),
204
+ user_agent: string({ max: 1024 })
205
+ });
206
+ const consentState = oneOf(consentStates);
207
+ const consent = (value, path, issues) => {
208
+ if (value === "not_applicable")
209
+ return;
210
+ if (!isObject(value)) {
211
+ {
212
+ issues.push({ path, message: 'expected { analytics, marketing } or "not_applicable"' });
213
+ return;
214
+ }
215
+ }
216
+ object({ analytics: consentState, marketing: consentState }, ["analytics"])(value, path, issues);
217
+ };
218
+ const minor = integer(-1000000000000000, 1000000000000000);
219
+ const id = string({ max: LIMITS.idLength });
220
+ const revenue = object({
221
+ amount: minor,
222
+ currency: string({ min: 3, max: 3, hint: "an ISO 4217 code" }),
223
+ entry_id: id,
224
+ kind: oneOf(revenueKinds),
225
+ fee: minor,
226
+ tax: minor,
227
+ refund_of: id,
228
+ product: id,
229
+ subscription: object({
230
+ id: string({ min: 1, max: LIMITS.idLength }),
231
+ interval: oneOf(billingIntervals),
232
+ interval_count: integer(1, 36),
233
+ period_start: dateTime,
234
+ period_end: dateTime
235
+ }, ["id"])
236
+ }, ["amount", "currency"]);
237
+ const externalIds = (value, path, issues) => {
238
+ if (!isObject(value)) {
239
+ issues.push({ path, message: "expected an object" });
240
+ return;
241
+ }
242
+ for (const [key, item] of Object.entries(value)) {
243
+ if (!externalIdTypes.includes(key)) {
244
+ issues.push({
245
+ path: join(path, key),
246
+ message: `expected one of ${externalIdTypes.join(", ")}`
247
+ });
248
+ } else if (item !== undefined) {
249
+ string({ min: 1, max: LIMITS.idLength })(item, join(path, key), issues);
250
+ }
251
+ }
252
+ };
253
+ const text = string({ max: 200, trim: true });
254
+ const touchpoint = object({
255
+ kind: oneOf(touchpointKinds),
256
+ channel: oneOf(touchpointChannels),
257
+ source: text,
258
+ medium: text,
259
+ campaign: text,
260
+ content: text,
261
+ campaign_id: text,
262
+ ad_group_id: text,
263
+ ad_id: text,
264
+ code: text,
265
+ answer: string({ max: 500, trim: true }),
266
+ external_ref: text
267
+ }, ["kind"]);
268
+ const event = object({
269
+ event_id: string({ max: 36, pattern: UUID, hint: "expected a UUID" }),
270
+ type: oneOf(["track", "page", "screen", "identify", "group"]),
271
+ event: string({
272
+ min: 1,
273
+ max: LIMITS.nameLength,
274
+ pattern: /^\P{Cc}+$/u,
275
+ hint: "no control characters"
276
+ }),
277
+ timestamp: dateTime,
278
+ anonymous_id: id,
279
+ user_id: id,
280
+ session_id: id,
281
+ group_id: id,
282
+ properties,
283
+ traits: properties,
284
+ external_ids: externalIds,
285
+ revenue,
286
+ context,
287
+ consent
288
+ }, ["event_id", "type", "event"]);
289
+ built = { event, touchpoint };
290
+ return built;
291
+ }
292
+ function validateEvent(value, path = "") {
293
+ const issues = [];
294
+ const { event, touchpoint } = rules();
295
+ event(value, path, issues);
296
+ if (isObject(value) && value.event === "$touchpoint") {
297
+ if (value.type !== "track") {
298
+ issues.push({ path: join(path, "type"), message: "$touchpoint is a track event" });
299
+ }
300
+ const props = isObject(value.properties) ? value.properties : {};
301
+ const before = issues.length;
302
+ touchpoint(props, join(path, "properties"), issues);
303
+ const source = typeof props.source === "string" ? props.source.trim() : "";
304
+ if (issues.length === before && !props.channel && !source) {
305
+ issues.push({
306
+ path: join(path, "properties.source"),
307
+ message: "a touchpoint needs a channel or a source"
308
+ });
309
+ }
310
+ }
311
+ return issues;
312
+ }
313
+ function validateBatch(value) {
314
+ const issues = [];
315
+ object({
316
+ key: string({ max: 100 }),
317
+ sent_at: dateTime,
318
+ stream: string({ min: 1, max: 64 }),
319
+ seq: integer(1, 2147483647)
320
+ })(value, "", issues);
321
+ if (!isObject(value))
322
+ return issues;
323
+ const events = value.events;
324
+ if (!Array.isArray(events)) {
325
+ issues.push({ path: "events", message: "expected a list of events" });
326
+ return issues;
327
+ }
328
+ if (events.length < 1 || events.length > LIMITS.eventsPerBatch) {
329
+ issues.push({ path: "events", message: `1 to ${LIMITS.eventsPerBatch} events per batch` });
330
+ }
331
+ events.forEach((item, index) => issues.push(...validateEvent(item, join("events", index))));
332
+ return issues;
333
+ }
334
+
335
+ // src/cli.ts
336
+ var file = process.argv[2];
337
+ var text = readFileSync(file && file !== "-" ? file : 0, "utf8");
338
+ var input;
339
+ try {
340
+ input = JSON.parse(text);
341
+ } catch (error) {
342
+ console.error(`✗ not JSON: ${String(error)}`);
343
+ process.exit(1);
344
+ }
345
+ var batches = Array.isArray(input) ? input : [input];
346
+ var failed = 0;
347
+ var events = 0;
348
+ batches.forEach((batch, index) => {
349
+ const issues = validateBatch(batch);
350
+ const list = typeof batch === "object" && batch !== null ? Reflect.get(batch, "events") : undefined;
351
+ if (Array.isArray(list))
352
+ events += list.length;
353
+ for (const issue of issues) {
354
+ failed += 1;
355
+ console.error(`✗ batch ${index}: ${issue.path || "(batch)"}: ${issue.message}`);
356
+ }
357
+ });
358
+ if (failed)
359
+ process.exit(1);
360
+ console.log(`✓ ${batches.length} batch(es), ${events} event(s): valid`);
@@ -0,0 +1,35 @@
1
+ import type { Consent, ConsentState } from "./wire.js";
2
+ /** What callers may pass for one purpose: a plain boolean from a custom banner, or a wire state. */
3
+ export type ConsentInput = boolean | ConsentState | undefined;
4
+ /** `true` → granted, `false` → denied, nothing → unknown. */
5
+ export declare const consentState: (value: ConsentInput) => ConsentState;
6
+ /** Consent for an event from booleans or states; `not_applicable` passes through. */
7
+ export declare function toConsent(input: "not_applicable" | {
8
+ analytics: ConsentInput;
9
+ marketing?: ConsentInput;
10
+ } | undefined, fallback?: Consent): Consent;
11
+ /**
12
+ * Which kind of key this is. Public keys (`mira_pk_…`) belong in browsers and apps; secret keys
13
+ * (`mira_sk_…`) only on servers, because they may send revenue and backfills.
14
+ */
15
+ export declare function keyKind(key: string | undefined): "public" | "secret" | "unknown";
16
+ /** Strips trailing slashes, so `host + "/v1/batch"` is always one URL. */
17
+ export declare const trimHost: (host: string) => string;
18
+ /** Answers worth retrying: timeouts, rate limits and server errors. Other 4xx are final. */
19
+ export declare const retryable: (status: number) => boolean;
20
+ /**
21
+ * Exponential backoff with full jitter, capped, but never sooner than the server asked
22
+ * (`Retry-After`, in milliseconds).
23
+ */
24
+ export declare function backoffMs(attempt: number, atLeastMs?: number, baseMs?: number, capMs?: number): number;
25
+ /** `Retry-After` in milliseconds: seconds or an HTTP date. 0 when absent or unreadable. */
26
+ export declare function retryAfterMs(header: string | null, now?: number): number;
27
+ /** A UUID of version 1 to 8 in the RFC 9562 variant, as the ingest accepts for `event_id`. */
28
+ export declare const isUuid: (value: string) => boolean;
29
+ /**
30
+ * A stable event id from your own key, such as `order:123:paid`. The same key always gives the
31
+ * same UUID, in every MIRA FIVE SDK and language, so a retried job, a backfill run twice, or the
32
+ * browser and the server reporting one conversion all count once.
33
+ * Format: UUIDv8 (RFC 9562) from the first 122 bits of SHA-256.
34
+ */
35
+ export declare function eventIdFor(key: string): Promise<string>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@mirafive/protocol`: the MIRA FIVE wire format and the helpers every SDK shares. No
3
+ * dependencies; runs in browsers, Node, Bun, Deno, workers and Convex.
4
+ *
5
+ * Build your own integration on it, or read `batch.schema.json` (JSON Schema of a batch) from
6
+ * another language.
7
+ */
8
+ export * from "./wire.js";
9
+ export { backoffMs, consentState, eventIdFor, isUuid, keyKind, retryable, retryAfterMs, toConsent, trimHost, type ConsentInput, } from "./helpers.js";
10
+ export { cleanExternalIds, cleanProperties } from "./properties.js";
11
+ export { uuidv7 } from "./uuid.js";
12
+ export { isDateTime, validateBatch, validateEvent, type Issue } from "./validate.js";