@getuserfeedback/protocol 0.6.1 → 0.7.2

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 (48) hide show
  1. package/dist/app-event.d.ts +659 -17
  2. package/dist/app-event.d.ts.map +1 -1
  3. package/dist/app-event.js +71 -8
  4. package/dist/flow-assignments.d.ts +4 -4
  5. package/dist/host/constants.d.ts +2 -2
  6. package/dist/host/constants.d.ts.map +1 -1
  7. package/dist/host/constants.js +3 -1
  8. package/dist/host/sdk-types.d.ts +73 -28
  9. package/dist/host/sdk-types.d.ts.map +1 -1
  10. package/dist/host/sdk.d.ts.map +1 -1
  11. package/dist/identity-type.d.ts +4 -2
  12. package/dist/identity-type.d.ts.map +1 -1
  13. package/dist/identity-type.js +4 -2
  14. package/dist/index.d.ts +2 -86
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -6
  17. package/dist/widget-commands.d.ts +102 -3
  18. package/dist/widget-commands.d.ts.map +1 -1
  19. package/dist/widget-commands.js +5 -0
  20. package/dist/widget-config.d.ts +5 -5
  21. package/package.json +22 -2
  22. package/src/app-event.ts +211 -0
  23. package/src/client-meta.ts +25 -0
  24. package/src/defaults.ts +4 -0
  25. package/src/errors.ts +58 -0
  26. package/src/flow-assignments.test.ts +63 -0
  27. package/src/flow-assignments.ts +125 -0
  28. package/src/host/command-dispatch.ts +59 -0
  29. package/src/host/command-envelope.ts +41 -0
  30. package/src/host/command-settlement.ts +121 -0
  31. package/src/host/constants.ts +103 -0
  32. package/src/host/host-event-contract.ts +277 -0
  33. package/src/host/index.ts +13 -0
  34. package/src/host/lazy-handle-client.ts +207 -0
  35. package/src/host/request-id.ts +3 -0
  36. package/src/host/sdk-types.ts +506 -0
  37. package/src/host/sdk.ts +43 -0
  38. package/src/host/unique-id.ts +17 -0
  39. package/src/identity-type.ts +102 -0
  40. package/src/index.ts +136 -0
  41. package/src/protocol-root.test.ts +476 -0
  42. package/src/public-grant-scope.ts +25 -0
  43. package/src/runtime-endpoints.ts +69 -0
  44. package/src/scopes.ts +79 -0
  45. package/src/trpc-envelope.ts +9 -0
  46. package/src/version-resolution.ts +18 -0
  47. package/src/widget-commands.ts +152 -0
  48. package/src/widget-config.ts +157 -0
@@ -0,0 +1,476 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { FlowDismissedPayload, FlowViewedPayload } from "./host";
3
+ import { createCommandEnvelope } from "./host";
4
+ import {
5
+ appEventCapabilitySchema,
6
+ appEventFlagSchema,
7
+ appEventFlowDismissedSchema,
8
+ appEventIdentityTypeSchema,
9
+ appEventPayloadSchema,
10
+ appEventSurveyViewedSchema,
11
+ appEventTrackSchema,
12
+ DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS,
13
+ flowVersionResolutionSchema,
14
+ getScopeMeta,
15
+ isPublicGrantScope,
16
+ isScope,
17
+ listScopes,
18
+ parseConfigureOptions,
19
+ parseInitOptions,
20
+ parsePublicCommand,
21
+ publicGrantScopeIdSchema,
22
+ publicWidgetCommandKindSchema,
23
+ SCOPE_IDS,
24
+ themeVersionResolutionSchema,
25
+ } from "./index";
26
+
27
+ describe("@getuserfeedback/protocol root contract", () => {
28
+ it("exports the canonical scope ids and metadata helpers", () => {
29
+ expect(SCOPE_IDS).toContain("functionality.storage");
30
+ expect(getScopeMeta("analytics.measurement").description).toContain(
31
+ "analytics events",
32
+ );
33
+ expect(listScopes()).toHaveLength(SCOPE_IDS.length);
34
+ expect(isScope("analytics.measurement")).toBe(true);
35
+ expect(isScope("not-a-scope")).toBe(false);
36
+ expect(isPublicGrantScope("analytics.measurement")).toBe(true);
37
+ expect(isPublicGrantScope("functionality.storage")).toBe(false);
38
+ expect(
39
+ publicGrantScopeIdSchema.safeParse("analytics.measurement").success,
40
+ ).toBe(true);
41
+ expect(
42
+ publicGrantScopeIdSchema.safeParse("functionality.storage").success,
43
+ ).toBe(false);
44
+ });
45
+
46
+ it("exports the canonical auto-detect color scheme defaults", () => {
47
+ expect(DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS).toEqual([
48
+ "class",
49
+ "data-theme",
50
+ ]);
51
+ });
52
+
53
+ it("parses widget-config flag and capability inputs via canonical app-event schemas", () => {
54
+ expect(
55
+ appEventFlagSchema.parse({
56
+ key: "new_checkout",
57
+ value: "variant_b",
58
+ variant: "variant_b",
59
+ }),
60
+ ).toEqual({
61
+ key: "new_checkout",
62
+ value: "variant_b",
63
+ variant: "variant_b",
64
+ });
65
+ expect(
66
+ appEventCapabilitySchema.parse({
67
+ key: "checkout.drawer",
68
+ source: "host-app",
69
+ }),
70
+ ).toEqual({
71
+ key: "checkout.drawer",
72
+ source: "host-app",
73
+ });
74
+ });
75
+
76
+ it("parses init options from the root contract", () => {
77
+ const result = parseInitOptions({
78
+ apiKey: "test-key",
79
+ colorScheme: {
80
+ autoDetectColorScheme: ["data-theme"],
81
+ },
82
+ defaultConsent: ["analytics.measurement"],
83
+ capabilities: ["gx.command.open.v1"],
84
+ flags: [
85
+ {
86
+ key: "new_checkout",
87
+ value: "variant_b",
88
+ variant: "variant_b",
89
+ provider: { name: "launchdarkly", environment: "production" },
90
+ },
91
+ ],
92
+ hostCapabilities: [{ key: "checkout.drawer", source: "host-app" }],
93
+ });
94
+
95
+ expect(result.apiKey).toBe("test-key");
96
+ expect(result.defaultConsent).toEqual(["analytics.measurement"]);
97
+ expect(result.capabilities).toEqual(["gx.command.open.v1"]);
98
+ expect(result.flags?.[0]).toMatchObject({
99
+ key: "new_checkout",
100
+ value: "variant_b",
101
+ });
102
+ expect(result.hostCapabilities?.[0]).toEqual({
103
+ key: "checkout.drawer",
104
+ source: "host-app",
105
+ });
106
+ });
107
+
108
+ it("accepts essential scopes in consent array inputs (CMP compatibility)", () => {
109
+ const result = parseInitOptions({
110
+ apiKey: "test-key",
111
+ defaultConsent: ["functionality.storage", "analytics.measurement"],
112
+ });
113
+
114
+ expect(result.defaultConsent).toEqual([
115
+ "functionality.storage",
116
+ "analytics.measurement",
117
+ ]);
118
+ });
119
+
120
+ it("parses configure options from the root contract", () => {
121
+ const result = parseConfigureOptions({
122
+ auth: {
123
+ jwt: {
124
+ token: "jwt-token",
125
+ },
126
+ },
127
+ consent: ["ads.storage"],
128
+ });
129
+
130
+ expect(result.auth?.jwt?.token).toBe("jwt-token");
131
+ expect(result.consent).toEqual(["ads.storage"]);
132
+ });
133
+
134
+ it("parses public commands from the root contract", () => {
135
+ const result = parsePublicCommand({
136
+ kind: "configure",
137
+ opts: {
138
+ consent: ["analytics.storage"],
139
+ },
140
+ });
141
+
142
+ expect(result.kind).toBe("configure");
143
+ if (result.kind === "configure") {
144
+ expect(result.opts.consent).toEqual(["analytics.storage"]);
145
+ }
146
+ });
147
+
148
+ it("parses public customer track commands from the root contract", () => {
149
+ const result = parsePublicCommand({
150
+ kind: "track",
151
+ origin: "customer",
152
+ event: "Checkout Started",
153
+ properties: { plan: "starter" },
154
+ });
155
+
156
+ expect(result).toEqual({
157
+ kind: "track",
158
+ origin: "customer",
159
+ event: "Checkout Started",
160
+ properties: { plan: "starter" },
161
+ });
162
+ });
163
+
164
+ it("exposes public widget command kind validation aligned with public payloads", () => {
165
+ expect(publicWidgetCommandKindSchema.safeParse("open").success).toBe(true);
166
+ expect(publicWidgetCommandKindSchema.safeParse("telemetry").success).toBe(
167
+ false,
168
+ );
169
+ });
170
+
171
+ it("strips legacy open.containerRequirement for enum values any and hostOnly", () => {
172
+ for (const containerRequirement of ["any", "hostOnly"] as const) {
173
+ const result = parsePublicCommand({
174
+ kind: "open",
175
+ flowId: "sur_123",
176
+ flowHandleId: "handle_open_123",
177
+ containerRequirement,
178
+ hideCloseButton: true,
179
+ });
180
+
181
+ expect(result.kind).toBe("open");
182
+ expect(result).toEqual({
183
+ kind: "open",
184
+ flowId: "sur_123",
185
+ flowHandleId: "handle_open_123",
186
+ hideCloseButton: true,
187
+ });
188
+ expect("containerRequirement" in result).toBe(false);
189
+ }
190
+ });
191
+
192
+ it("parses public identify commands from the root contract", () => {
193
+ const withUserId = parsePublicCommand({
194
+ kind: "identify",
195
+ userId: "user_123",
196
+ traits: {
197
+ email: "user@example.com",
198
+ },
199
+ });
200
+ const withTraitsOnly = parsePublicCommand({
201
+ kind: "identify",
202
+ traits: {
203
+ plan: "pro",
204
+ },
205
+ });
206
+
207
+ expect(withUserId.kind).toBe("identify");
208
+ if (withUserId.kind === "identify" && "userId" in withUserId) {
209
+ expect(withUserId.userId).toBe("user_123");
210
+ expect(withUserId.traits).toEqual({
211
+ email: "user@example.com",
212
+ });
213
+ }
214
+
215
+ expect(withTraitsOnly.kind).toBe("identify");
216
+ if (withTraitsOnly.kind === "identify" && !("userId" in withTraitsOnly)) {
217
+ expect(withTraitsOnly.traits).toEqual({
218
+ plan: "pro",
219
+ });
220
+ }
221
+ });
222
+
223
+ it("builds canonical public command envelopes from the host contract", () => {
224
+ const envelope = createCommandEnvelope({
225
+ command: {
226
+ kind: "configure",
227
+ opts: {
228
+ consent: ["analytics.storage"],
229
+ },
230
+ },
231
+ instanceId: "instance_123",
232
+ clientMeta: {
233
+ loader: "sdk",
234
+ transport: "snippet",
235
+ },
236
+ });
237
+
238
+ expect(envelope.version).toBe("1");
239
+ expect(envelope.command.kind).toBe("configure");
240
+ expect(envelope.instanceId).toBe("instance_123");
241
+ expect(envelope.idempotencyKey).toBe(envelope.requestId);
242
+ expect(envelope.clientMeta).toEqual({
243
+ loader: "sdk",
244
+ transport: "snippet",
245
+ });
246
+ });
247
+
248
+ it("normalizes blank optional envelope identifiers", () => {
249
+ const envelope = createCommandEnvelope({
250
+ command: {
251
+ kind: "configure",
252
+ opts: {
253
+ consent: ["analytics.storage"],
254
+ },
255
+ },
256
+ instanceId: " ",
257
+ requestId: " request_123 ",
258
+ idempotencyKey: " ",
259
+ });
260
+
261
+ expect(envelope.requestId).toBe("request_123");
262
+ expect(envelope.idempotencyKey).toBe("request_123");
263
+ expect("instanceId" in envelope).toBe(false);
264
+ });
265
+
266
+ it("rejects internal navigation context commands from the public contract", () => {
267
+ expect(() =>
268
+ parsePublicCommand({
269
+ kind: "hostContextUpdated",
270
+ context: {
271
+ url: "https://example.com/pricing",
272
+ },
273
+ }),
274
+ ).toThrow();
275
+ });
276
+
277
+ it("parses public API flow and theme version resolution payloads", () => {
278
+ expect(
279
+ flowVersionResolutionSchema.parse({ flowVersionId: "fv_abc" }),
280
+ ).toEqual({ flowVersionId: "fv_abc" });
281
+ expect(
282
+ themeVersionResolutionSchema.parse({ themeVersionId: "tv_xyz" }),
283
+ ).toEqual({ themeVersionId: "tv_xyz" });
284
+ expect(() =>
285
+ flowVersionResolutionSchema.parse({ flowVersionId: " " }),
286
+ ).toThrow();
287
+ });
288
+
289
+ it("parses app event track and payload shapes", () => {
290
+ const track = appEventTrackSchema.parse({
291
+ event: "Flow Viewed",
292
+ references: { surveyId: "sur_1" },
293
+ identities: [{ type: "anonymousId", value: "anon" }],
294
+ context: {
295
+ flags: [
296
+ {
297
+ key: "new_checkout",
298
+ value: "variant_b",
299
+ variant: "variant_b",
300
+ provider: {
301
+ name: "launchdarkly",
302
+ project: "mobile",
303
+ environment: "production",
304
+ },
305
+ },
306
+ ],
307
+ capabilities: [{ key: "new_checkout" }],
308
+ },
309
+ });
310
+ expect(track.event).toBe("Flow Viewed");
311
+ expect(track.origin).toBe("system");
312
+ expect(track.references.surveyId).toBe("sur_1");
313
+ expect(track.context?.flags?.[0]).toMatchObject({
314
+ key: "new_checkout",
315
+ value: "variant_b",
316
+ });
317
+ expect(track.context?.capabilities?.[0]).toEqual({
318
+ key: "new_checkout",
319
+ });
320
+
321
+ const viewed = appEventPayloadSchema.parse({
322
+ origin: "system",
323
+ event: "Flow Viewed",
324
+ references: { surveyId: "sur_2" },
325
+ });
326
+ expect(viewed.event).toBe("Flow Viewed");
327
+
328
+ const customer = appEventTrackSchema.parse({
329
+ origin: "customer",
330
+ event: "Checkout Started",
331
+ properties: {
332
+ plan: "starter",
333
+ step: 1,
334
+ renewal: null,
335
+ items: ["seat", { count: 2 }],
336
+ },
337
+ });
338
+ expect(customer).toEqual({
339
+ origin: "customer",
340
+ event: "Checkout Started",
341
+ properties: {
342
+ plan: "starter",
343
+ step: 1,
344
+ renewal: null,
345
+ items: ["seat", { count: 2 }],
346
+ },
347
+ });
348
+
349
+ for (const properties of [
350
+ { callback: () => undefined },
351
+ { amount: Number.NaN },
352
+ { amount: Number.POSITIVE_INFINITY },
353
+ { orderId: BigInt(1) },
354
+ { renewedAt: new globalThis.Date("2026-05-08T00:00:00.000Z") },
355
+ ]) {
356
+ expect(() =>
357
+ appEventTrackSchema.parse({
358
+ origin: "customer",
359
+ event: "Checkout Started",
360
+ properties,
361
+ }),
362
+ ).toThrow();
363
+ }
364
+
365
+ expect(() =>
366
+ appEventTrackSchema.parse({
367
+ event: "Unknown",
368
+ references: { surveyId: "sur_1" },
369
+ }),
370
+ ).toThrow();
371
+ expect(() =>
372
+ appEventTrackSchema.parse({
373
+ origin: "customer",
374
+ event: "Flow Viewed",
375
+ }),
376
+ ).toThrow();
377
+ expect(() =>
378
+ appEventTrackSchema.parse({
379
+ origin: "customer",
380
+ event: "flow viewed",
381
+ }),
382
+ ).toThrow();
383
+ });
384
+
385
+ it("keeps flag value validation separate from track properties", () => {
386
+ expect(
387
+ appEventFlagSchema.parse({
388
+ key: "new_checkout",
389
+ value: {
390
+ variant: "treatment",
391
+ enabled: true,
392
+ rollout: 50,
393
+ },
394
+ }),
395
+ ).toEqual({
396
+ key: "new_checkout",
397
+ value: {
398
+ variant: "treatment",
399
+ enabled: true,
400
+ rollout: 50,
401
+ },
402
+ });
403
+
404
+ expect(() =>
405
+ appEventFlagSchema.parse({
406
+ key: "new_checkout",
407
+ value: { callback: () => undefined },
408
+ }),
409
+ ).toThrow();
410
+ });
411
+
412
+ it("keeps exported system app event schemas compatible with legacy no-origin inputs", () => {
413
+ const viewed = appEventSurveyViewedSchema.parse({
414
+ event: "Flow Viewed",
415
+ references: { surveyId: "sur_1" },
416
+ });
417
+ const dismissed = appEventFlowDismissedSchema.parse({
418
+ event: "Flow Dismissed",
419
+ references: { surveyId: "sur_2" },
420
+ });
421
+
422
+ expect(viewed.origin).toBe("system");
423
+ expect(dismissed.origin).toBe("system");
424
+ });
425
+
426
+ it("keeps legacy system app event SDK input types origin-optional", () => {
427
+ const viewed: FlowViewedPayload = {
428
+ event: "Flow Viewed",
429
+ references: { surveyId: "sur_1" },
430
+ };
431
+ const dismissed: FlowDismissedPayload = {
432
+ event: "Flow Dismissed",
433
+ references: { surveyId: "sur_2" },
434
+ };
435
+
436
+ expect(viewed.origin).toBeUndefined();
437
+ expect(dismissed.origin).toBeUndefined();
438
+ });
439
+
440
+ it("suggests canonical identity types for app event aliases", () => {
441
+ expect(() =>
442
+ appEventTrackSchema.parse({
443
+ event: "Flow Viewed",
444
+ references: { surveyId: "sur_1" },
445
+ identities: [{ type: "email", value: "person@example.com" }],
446
+ }),
447
+ ).toThrow(/traits\.email/);
448
+ });
449
+
450
+ it("keeps identity type guidance on invalid app event identity values", () => {
451
+ expect(() => appEventIdentityTypeSchema.safeParse("email")).not.toThrow();
452
+
453
+ const result = appEventIdentityTypeSchema.safeParse("email");
454
+
455
+ expect(result.success).toBe(false);
456
+ if (!result.success) {
457
+ expect(result.error.issues).toEqual([
458
+ expect.objectContaining({
459
+ code: "custom",
460
+ message:
461
+ 'Unsupported identity type "email". Use "traits.email" instead.',
462
+ }),
463
+ ]);
464
+ }
465
+
466
+ expect(() => appEventIdentityTypeSchema.parse("email")).toThrow(
467
+ /traits\.email/,
468
+ );
469
+ });
470
+
471
+ it("accepts first-class custom app event identity types", () => {
472
+ const result = appEventIdentityTypeSchema.parse("custom.shopifyCustomerId");
473
+
474
+ expect(result).toBe("custom.shopifyCustomerId");
475
+ });
476
+ });
@@ -0,0 +1,25 @@
1
+ import * as z from "zod/mini";
2
+
3
+ const publicGrantScopeIds: readonly [
4
+ "analytics.storage",
5
+ "analytics.measurement",
6
+ "personalization.storage",
7
+ "ads.storage",
8
+ "ads.user_data",
9
+ "ads.personalization",
10
+ ] = [
11
+ "analytics.storage",
12
+ "analytics.measurement",
13
+ "personalization.storage",
14
+ "ads.storage",
15
+ "ads.user_data",
16
+ "ads.personalization",
17
+ ];
18
+
19
+ export const publicGrantScopeIdSchema = z.enum(publicGrantScopeIds);
20
+
21
+ export type PublicGrantScope = z.output<typeof publicGrantScopeIdSchema>;
22
+
23
+ export function isPublicGrantScope(value: unknown): value is PublicGrantScope {
24
+ return publicGrantScopeIdSchema.safeParse(value).success;
25
+ }
@@ -0,0 +1,69 @@
1
+ import * as z from "zod/mini";
2
+
3
+ const absoluteUrlSchema = z.string().check(z.trim(), z.url());
4
+
5
+ export const runtimeEndpointsSchema = z.strictObject({
6
+ apiUrl: z.optional(absoluteUrlSchema),
7
+ coreUrl: z.optional(absoluteUrlSchema),
8
+ });
9
+
10
+ export type RuntimeEndpoints = z.output<typeof runtimeEndpointsSchema>;
11
+
12
+ const parseAbsoluteUrl = (value: string | undefined): URL | null => {
13
+ if (!value) {
14
+ return null;
15
+ }
16
+ try {
17
+ return new URL(value);
18
+ } catch {
19
+ return null;
20
+ }
21
+ };
22
+
23
+ const normalizeOrigin = (value: string | undefined): string | null => {
24
+ const parsed = parseAbsoluteUrl(value);
25
+ if (!parsed) {
26
+ return null;
27
+ }
28
+ return parsed.origin;
29
+ };
30
+
31
+ export const resolveCoreUrl = (
32
+ runtimeEndpoints: RuntimeEndpoints | undefined,
33
+ ): URL | null => {
34
+ return parseAbsoluteUrl(runtimeEndpoints?.coreUrl);
35
+ };
36
+
37
+ export const resolveApiBaseUrl = (
38
+ runtimeEndpoints: RuntimeEndpoints | undefined,
39
+ ): URL | null => {
40
+ const apiUrl = runtimeEndpoints?.apiUrl;
41
+ const parsed = parseAbsoluteUrl(apiUrl);
42
+ if (!parsed) {
43
+ return null;
44
+ }
45
+ if (!parsed.pathname || parsed.pathname === "/") {
46
+ return null;
47
+ }
48
+ if (!parsed.pathname.endsWith("/")) {
49
+ parsed.pathname = `${parsed.pathname}/`;
50
+ }
51
+ return parsed;
52
+ };
53
+
54
+ export const resolveTrpcBaseUrl = (
55
+ runtimeEndpoints: RuntimeEndpoints | undefined,
56
+ ): URL | null => {
57
+ const apiBaseUrl = resolveApiBaseUrl(runtimeEndpoints);
58
+ if (!apiBaseUrl) {
59
+ return null;
60
+ }
61
+ const normalizedApiPath = apiBaseUrl.pathname.endsWith("/")
62
+ ? apiBaseUrl.pathname.slice(0, -1)
63
+ : apiBaseUrl.pathname;
64
+ if (!normalizedApiPath.endsWith("/v1")) {
65
+ return null;
66
+ }
67
+ const trpcPath = normalizedApiPath.slice(0, -"/v1".length) || "/";
68
+ return new URL(trpcPath, `${normalizeOrigin(apiBaseUrl.toString()) ?? ""}/`);
69
+ };
package/src/scopes.ts ADDED
@@ -0,0 +1,79 @@
1
+ import * as z from "zod/mini";
2
+
3
+ export const SCOPE_IDS: readonly [
4
+ "functionality.storage",
5
+ "security.storage",
6
+ "analytics.storage",
7
+ "analytics.measurement",
8
+ "personalization.storage",
9
+ "ads.storage",
10
+ "ads.user_data",
11
+ "ads.personalization",
12
+ ] = [
13
+ "functionality.storage",
14
+ "security.storage",
15
+ "analytics.storage",
16
+ "analytics.measurement",
17
+ "personalization.storage",
18
+ "ads.storage",
19
+ "ads.user_data",
20
+ "ads.personalization",
21
+ ];
22
+
23
+ export const scopeIdSchema = z.enum(SCOPE_IDS);
24
+
25
+ export type Scope = (typeof SCOPE_IDS)[number];
26
+
27
+ export function isScope(value: unknown): value is Scope {
28
+ return scopeIdSchema.safeParse(value).success;
29
+ }
30
+
31
+ export type ScopeMeta = {
32
+ description: string;
33
+ };
34
+
35
+ const scopeMetaById: Record<Scope, ScopeMeta> = {
36
+ "functionality.storage": {
37
+ description:
38
+ "Allow essential storage for core widget behavior (e.g., frequency capping, dismissals, draft answers).",
39
+ },
40
+ "security.storage": {
41
+ description:
42
+ "Allow storage used for fraud/abuse prevention and integrity checks.",
43
+ },
44
+ "analytics.storage": {
45
+ description:
46
+ "Allow storage for analytics identifiers enabling consistent cross-session measurement.",
47
+ },
48
+ "analytics.measurement": {
49
+ description:
50
+ "Allow sending analytics events; can operate without storage when needed.",
51
+ },
52
+ "personalization.storage": {
53
+ description:
54
+ "Allow storage used to personalize the widget UI/targeting (non-ads).",
55
+ },
56
+ "ads.storage": {
57
+ description:
58
+ "Allow storage for advertising identifiers and conversion cookies.",
59
+ },
60
+ "ads.user_data": {
61
+ description:
62
+ "Allow sending user data to ad platforms when legally permitted.",
63
+ },
64
+ "ads.personalization": {
65
+ description:
66
+ "Allow personalized advertising signals and interest-based targeting.",
67
+ },
68
+ };
69
+
70
+ export function getScopeMeta(id: Scope): ScopeMeta {
71
+ return scopeMetaById[id];
72
+ }
73
+
74
+ export function listScopes(): ReadonlyArray<{ id: Scope } & ScopeMeta> {
75
+ return SCOPE_IDS.map((id) => ({
76
+ id,
77
+ ...getScopeMeta(id),
78
+ }));
79
+ }
@@ -0,0 +1,9 @@
1
+ import * as z from "zod/mini";
2
+
3
+ export const trpcSuccessResponseSchema = z.object({
4
+ result: z.object({
5
+ data: z.unknown(),
6
+ }),
7
+ });
8
+
9
+ export type TrpcSuccessResponse = z.output<typeof trpcSuccessResponseSchema>;
@@ -0,0 +1,18 @@
1
+ import * as z from "zod/mini";
2
+
3
+ const versionIdSchema = z.string().check(z.trim(), z.minLength(1));
4
+
5
+ export const flowVersionResolutionSchema = z.object({
6
+ flowVersionId: versionIdSchema,
7
+ });
8
+
9
+ export const themeVersionResolutionSchema = z.object({
10
+ themeVersionId: versionIdSchema,
11
+ });
12
+
13
+ export type FlowVersionResolution = z.output<
14
+ typeof flowVersionResolutionSchema
15
+ >;
16
+ export type ThemeVersionResolution = z.output<
17
+ typeof themeVersionResolutionSchema
18
+ >;