@storewatch/schema 0.0.0-stage → 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/dist/index.js ADDED
@@ -0,0 +1,217 @@
1
+ import {
2
+ CANONICAL_EVENTS,
3
+ EPHEMERAL_EVENTS,
4
+ EVENT_NAME_PATTERN,
5
+ LIMITS,
6
+ SERVER_AUTHORITATIVE_EVENTS,
7
+ currencyExponent,
8
+ isCanonicalEvent,
9
+ money,
10
+ toMajor
11
+ } from "./chunk-MEHWUFJG.js";
12
+
13
+ // src/primitives.ts
14
+ import { z } from "zod";
15
+ var knownCurrencies = new Set(Intl.supportedValuesOf("currency"));
16
+ var CurrencySchema = z.string().regex(/^[A-Z]{3}$/, "must be an upper-case ISO-4217 code").refine((c) => knownCurrencies.has(c), "unknown ISO-4217 currency");
17
+ var MoneySchema = z.strictObject({
18
+ amount: z.number().int("money amounts are integer minor units").safe(),
19
+ currency: CurrencySchema
20
+ });
21
+ var IdSchema = z.string().min(1).max(LIMITS.idMaxLength);
22
+ var ShortString = z.string().max(LIMITS.stringMaxLength);
23
+ var UrlString = z.string().max(LIMITS.urlMaxLength);
24
+ function jsonDepth(value) {
25
+ if (value === null || typeof value !== "object") return 0;
26
+ const children = Array.isArray(value) ? value : Object.values(value);
27
+ let max = 0;
28
+ for (const child of children) max = Math.max(max, jsonDepth(child));
29
+ return max + 1;
30
+ }
31
+ var JsonValueSchema = z.lazy(
32
+ () => z.union([
33
+ z.string().max(LIMITS.stringMaxLength),
34
+ z.number().refine(Number.isFinite, "must be finite"),
35
+ z.boolean(),
36
+ z.null(),
37
+ z.array(JsonValueSchema),
38
+ z.record(z.string().max(LIMITS.eventNameMaxLength), JsonValueSchema)
39
+ ])
40
+ );
41
+ var utf8 = new TextEncoder();
42
+ var PropertiesSchema = z.record(z.string().max(LIMITS.eventNameMaxLength), JsonValueSchema).superRefine((value, ctx) => {
43
+ if (jsonDepth(value) > LIMITS.propertiesMaxDepth) {
44
+ ctx.addIssue({ code: "custom", message: `nesting deeper than ${LIMITS.propertiesMaxDepth}` });
45
+ }
46
+ if (utf8.encode(JSON.stringify(value)).length > LIMITS.propertiesMaxBytes) {
47
+ ctx.addIssue({ code: "custom", message: `larger than ${LIMITS.propertiesMaxBytes} bytes` });
48
+ }
49
+ });
50
+
51
+ // src/events.ts
52
+ import { z as z2 } from "zod";
53
+ var props = z2.looseObject;
54
+ var OrderItemSchema = z2.looseObject({
55
+ productId: IdSchema,
56
+ variantId: IdSchema.optional(),
57
+ name: ShortString.optional(),
58
+ quantity: z2.number().int().positive(),
59
+ price: MoneySchema
60
+ });
61
+ var CANONICAL_PROPERTY_SCHEMAS = {
62
+ page_viewed: props({ pageType: z2.string().max(32).optional() }),
63
+ product_viewed: props({
64
+ productId: IdSchema,
65
+ variantId: IdSchema.optional(),
66
+ name: ShortString.optional(),
67
+ price: MoneySchema.optional()
68
+ }),
69
+ collection_viewed: props({ collectionId: IdSchema, name: ShortString.optional() }),
70
+ search_performed: props({
71
+ query: z2.string().max(LIMITS.searchQueryMaxLength),
72
+ resultCount: z2.number().int().nonnegative().optional()
73
+ }),
74
+ add_to_cart: props({
75
+ productId: IdSchema,
76
+ variantId: IdSchema.optional(),
77
+ name: ShortString.optional(),
78
+ quantity: z2.number().int().positive(),
79
+ cartId: IdSchema.optional(),
80
+ price: MoneySchema.optional(),
81
+ cartValue: MoneySchema.optional()
82
+ }),
83
+ remove_from_cart: props({
84
+ productId: IdSchema,
85
+ variantId: IdSchema.optional(),
86
+ quantity: z2.number().int().positive(),
87
+ cartId: IdSchema.optional(),
88
+ cartValue: MoneySchema.optional()
89
+ }),
90
+ cart_updated: props({
91
+ cartId: IdSchema,
92
+ cartValue: MoneySchema,
93
+ itemCount: z2.number().int().nonnegative()
94
+ }),
95
+ checkout_started: props({ checkoutId: IdSchema, cartId: IdSchema.optional(), cartValue: MoneySchema }),
96
+ checkout_step_viewed: props({ checkoutId: IdSchema, step: z2.string().max(32) }),
97
+ // Deliberately carries no field contents: only that the step happened.
98
+ customer_information_entered: z2.strictObject({ checkoutId: IdSchema }),
99
+ shipping_selected: props({
100
+ checkoutId: IdSchema,
101
+ method: ShortString.optional(),
102
+ shippingCost: MoneySchema.optional()
103
+ }),
104
+ payment_started: props({ checkoutId: IdSchema, provider: z2.string().max(64).optional() }),
105
+ payment_failed: props({
106
+ checkoutId: IdSchema,
107
+ provider: z2.string().max(64).optional(),
108
+ // A provider error *code*, never a free-text message (those leak PII).
109
+ reason: z2.string().max(64).optional(),
110
+ amount: MoneySchema.optional()
111
+ }),
112
+ payment_authorized: props({
113
+ checkoutId: IdSchema,
114
+ provider: z2.string().max(64).optional(),
115
+ amount: MoneySchema
116
+ }),
117
+ order_completed: props({
118
+ orderId: IdSchema,
119
+ checkoutId: IdSchema.optional(),
120
+ cartId: IdSchema.optional(),
121
+ total: MoneySchema,
122
+ items: z2.array(OrderItemSchema).max(LIMITS.orderMaxItems)
123
+ }),
124
+ order_cancelled: props({ orderId: IdSchema, reason: z2.string().max(64).optional() }),
125
+ refund_created: props({ orderId: IdSchema, amount: MoneySchema }),
126
+ session_heartbeat: props({})
127
+ };
128
+
129
+ // src/envelope.ts
130
+ import { z as z3 } from "zod";
131
+ var HexId = (length) => z3.string().regex(new RegExp(`^[0-9a-f]{${length}}$`));
132
+ var ContextSchema = z3.object({
133
+ page: z3.object({
134
+ url: UrlString.optional(),
135
+ path: UrlString.optional(),
136
+ referrer: UrlString.optional(),
137
+ title: ShortString.optional()
138
+ }).optional(),
139
+ utm: z3.object({
140
+ source: ShortString.optional(),
141
+ medium: ShortString.optional(),
142
+ campaign: ShortString.optional(),
143
+ term: ShortString.optional(),
144
+ content: ShortString.optional()
145
+ }).optional(),
146
+ locale: z3.string().max(35).optional(),
147
+ timezone: z3.string().max(64).optional(),
148
+ currency: CurrencySchema.optional(),
149
+ screen: z3.object({ width: z3.number().int().nonnegative(), height: z3.number().int().nonnegative() }).optional(),
150
+ trace: z3.object({ traceId: HexId(32), spanId: HexId(16).optional() }).optional(),
151
+ library: z3.object({ name: z3.string().max(64), version: z3.string().max(32) }).optional(),
152
+ consent: z3.enum(["pending", "granted", "denied"]).optional()
153
+ });
154
+ var WireEventSchema = z3.object({
155
+ eventId: z3.uuid(),
156
+ name: z3.string().regex(EVENT_NAME_PATTERN, "event names are snake_case, \u2264 64 chars"),
157
+ timestamp: z3.iso.datetime({ offset: true }),
158
+ sessionId: z3.string().min(1).max(LIMITS.sessionIdMaxLength),
159
+ anonymousId: z3.string().min(1).max(LIMITS.sessionIdMaxLength).optional(),
160
+ customerId: IdSchema.optional(),
161
+ properties: PropertiesSchema.optional(),
162
+ context: ContextSchema.optional()
163
+ });
164
+ var WireBatchSchema = z3.object({
165
+ sentAt: z3.iso.datetime({ offset: true }).optional(),
166
+ key: z3.string().max(128).optional(),
167
+ // Elements are validated one by one so a bad event doesn't sink the batch.
168
+ batch: z3.array(z3.unknown()).min(1).max(LIMITS.batchMaxEvents)
169
+ });
170
+ function toIssues(error, prefix = "") {
171
+ return error.issues.map((i) => ({
172
+ path: [prefix, ...i.path.map(String)].filter(Boolean).join("."),
173
+ message: i.message
174
+ }));
175
+ }
176
+ function validateEvent(raw) {
177
+ const envelope = WireEventSchema.safeParse(raw);
178
+ if (!envelope.success) {
179
+ const eventId = typeof raw === "object" && raw !== null && "eventId" in raw && typeof raw.eventId === "string" ? raw.eventId : void 0;
180
+ return { ok: false, eventId, issues: toIssues(envelope.error) };
181
+ }
182
+ const { properties = {}, ...rest } = envelope.data;
183
+ if (!isCanonicalEvent(rest.name)) {
184
+ return { ok: true, event: { ...rest, kind: "custom", properties } };
185
+ }
186
+ const parsed = CANONICAL_PROPERTY_SCHEMAS[rest.name].safeParse(properties);
187
+ if (!parsed.success) {
188
+ return { ok: false, eventId: rest.eventId, issues: toIssues(parsed.error, "properties") };
189
+ }
190
+ return {
191
+ ok: true,
192
+ event: { ...rest, kind: "canonical", name: rest.name, properties: parsed.data }
193
+ };
194
+ }
195
+ export {
196
+ CANONICAL_EVENTS,
197
+ CANONICAL_PROPERTY_SCHEMAS,
198
+ ContextSchema,
199
+ CurrencySchema,
200
+ EPHEMERAL_EVENTS,
201
+ EVENT_NAME_PATTERN,
202
+ IdSchema,
203
+ LIMITS,
204
+ MoneySchema,
205
+ PropertiesSchema,
206
+ SERVER_AUTHORITATIVE_EVENTS,
207
+ ShortString,
208
+ UrlString,
209
+ WireBatchSchema,
210
+ WireEventSchema,
211
+ currencyExponent,
212
+ isCanonicalEvent,
213
+ jsonDepth,
214
+ money,
215
+ toMajor,
216
+ validateEvent
217
+ };
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ export declare const CurrencySchema: z.ZodString;
3
+ export declare const MoneySchema: z.ZodObject<{
4
+ amount: z.ZodNumber;
5
+ currency: z.ZodString;
6
+ }, z.core.$strict>;
7
+ export declare const IdSchema: z.ZodString;
8
+ export declare const ShortString: z.ZodString;
9
+ export declare const UrlString: z.ZodString;
10
+ export type Json = string | number | boolean | null | Json[] | {
11
+ [key: string]: Json;
12
+ };
13
+ /** Depth of a JSON value: scalars are 0, `{a:1}` is 1. */
14
+ export declare function jsonDepth(value: unknown): number;
15
+ /** Free-form properties with byte-size and depth limits. */
16
+ export declare const PropertiesSchema: z.ZodRecord<z.ZodString, z.ZodType<Json, unknown, z.core.$ZodTypeInternals<Json, unknown>>>;
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ export declare const CurrencySchema: z.ZodString;
3
+ export declare const MoneySchema: z.ZodObject<{
4
+ amount: z.ZodNumber;
5
+ currency: z.ZodString;
6
+ }, z.core.$strict>;
7
+ export declare const IdSchema: z.ZodString;
8
+ export declare const ShortString: z.ZodString;
9
+ export declare const UrlString: z.ZodString;
10
+ export type Json = string | number | boolean | null | Json[] | {
11
+ [key: string]: Json;
12
+ };
13
+ /** Depth of a JSON value: scalars are 0, `{a:1}` is 1. */
14
+ export declare function jsonDepth(value: unknown): number;
15
+ /** Free-form properties with byte-size and depth limits. */
16
+ export declare const PropertiesSchema: z.ZodRecord<z.ZodString, z.ZodType<Json, unknown, z.core.$ZodTypeInternals<Json, unknown>>>;
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@storewatch/schema",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Storewatch canonical commerce event contract.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.cts",
16
+ "default": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "./constants": {
20
+ "import": {
21
+ "types": "./dist/constants.d.ts",
22
+ "default": "./dist/constants.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/constants.d.cts",
26
+ "default": "./dist/constants.cjs"
27
+ }
28
+ }
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "dependencies": {
37
+ "zod": "^4.6.7"
38
+ },
39
+ "devDependencies": {
40
+ "tsup": "^9.0.0",
41
+ "@storewatch/config": "0.0.0"
42
+ },
43
+ "keywords": [
44
+ "storewatch",
45
+ "analytics",
46
+ "ecommerce"
47
+ ],
48
+ "scripts": {
49
+ "typecheck": "tsc -p .",
50
+ "test": "vitest run",
51
+ "build": "rm -rf dist && tsup && tsc -p tsconfig.build.json && node ../../scripts/dual-dts.mjs dist"
52
+ }
6
53
  }