@getuserfeedback/protocol 0.6.0 → 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 (53) 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/errors.d.ts +73 -0
  5. package/dist/errors.d.ts.map +1 -0
  6. package/dist/errors.js +43 -0
  7. package/dist/flow-assignments.d.ts +83 -4
  8. package/dist/flow-assignments.d.ts.map +1 -1
  9. package/dist/flow-assignments.js +1 -0
  10. package/dist/host/constants.d.ts +2 -2
  11. package/dist/host/constants.d.ts.map +1 -1
  12. package/dist/host/constants.js +3 -1
  13. package/dist/host/sdk-types.d.ts +73 -28
  14. package/dist/host/sdk-types.d.ts.map +1 -1
  15. package/dist/host/sdk.d.ts.map +1 -1
  16. package/dist/identity-type.d.ts +4 -2
  17. package/dist/identity-type.d.ts.map +1 -1
  18. package/dist/identity-type.js +4 -2
  19. package/dist/index.d.ts +4 -86
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -6
  22. package/dist/widget-commands.d.ts +102 -3
  23. package/dist/widget-commands.d.ts.map +1 -1
  24. package/dist/widget-commands.js +5 -0
  25. package/dist/widget-config.d.ts +5 -5
  26. package/package.json +22 -2
  27. package/src/app-event.ts +211 -0
  28. package/src/client-meta.ts +25 -0
  29. package/src/defaults.ts +4 -0
  30. package/src/errors.ts +58 -0
  31. package/src/flow-assignments.test.ts +63 -0
  32. package/src/flow-assignments.ts +125 -0
  33. package/src/host/command-dispatch.ts +59 -0
  34. package/src/host/command-envelope.ts +41 -0
  35. package/src/host/command-settlement.ts +121 -0
  36. package/src/host/constants.ts +103 -0
  37. package/src/host/host-event-contract.ts +277 -0
  38. package/src/host/index.ts +13 -0
  39. package/src/host/lazy-handle-client.ts +207 -0
  40. package/src/host/request-id.ts +3 -0
  41. package/src/host/sdk-types.ts +506 -0
  42. package/src/host/sdk.ts +43 -0
  43. package/src/host/unique-id.ts +17 -0
  44. package/src/identity-type.ts +102 -0
  45. package/src/index.ts +136 -0
  46. package/src/protocol-root.test.ts +476 -0
  47. package/src/public-grant-scope.ts +25 -0
  48. package/src/runtime-endpoints.ts +69 -0
  49. package/src/scopes.ts +79 -0
  50. package/src/trpc-envelope.ts +9 -0
  51. package/src/version-resolution.ts +18 -0
  52. package/src/widget-commands.ts +152 -0
  53. package/src/widget-config.ts +157 -0
package/src/index.ts ADDED
@@ -0,0 +1,136 @@
1
+ export {
2
+ type AppEventCapability,
3
+ type AppEventFlag,
4
+ type AppEventJsonValue,
5
+ type AppEventPayload,
6
+ type AppEventTrackInput,
7
+ appEventBaseSchema,
8
+ appEventCapabilitySchema,
9
+ appEventContextSchema,
10
+ appEventCustomerTrackSchema,
11
+ appEventFlagProviderSchema,
12
+ appEventFlagSchema,
13
+ appEventFlowDismissedSchema,
14
+ appEventIdentitySchema,
15
+ appEventIdentityTypeSchema,
16
+ appEventPayloadSchema,
17
+ appEventSurveyViewedSchema,
18
+ appEventSystemBaseSchema,
19
+ appEventSystemTrackSchema,
20
+ appEventTrackSchema,
21
+ reservedSystemAppEventNames,
22
+ } from "./app-event.js";
23
+ export {
24
+ type ClientMeta,
25
+ clientMetaSchema,
26
+ } from "./client-meta.js";
27
+ export { DEFAULT_AUTO_DETECT_COLOR_SCHEME_ATTRS } from "./defaults.js";
28
+ export type { ErrorEvent, ProtocolErrorCode } from "./errors.js";
29
+ export {
30
+ ErrorCategory,
31
+ ErrorCategorySchema,
32
+ ErrorEventSchema,
33
+ ProtocolError,
34
+ } from "./errors.js";
35
+ export {
36
+ type ClaimPendingFlowAssignmentsRequest,
37
+ type ClaimPendingFlowAssignmentsResponse,
38
+ claimPendingFlowAssignmentsRequestSchema,
39
+ claimPendingFlowAssignmentsResponseSchema,
40
+ type FlowAssignmentRecipientRequest,
41
+ type FlowAssignmentRecord,
42
+ type FlowAssignmentStatus,
43
+ type FlowAssignmentTargetingContext,
44
+ flowAssignmentRecipientRequestSchema,
45
+ flowAssignmentRecordSchema,
46
+ flowAssignmentStatusSchema,
47
+ flowAssignmentTargetingContextSchema,
48
+ type ListPendingFlowAssignmentsResponse,
49
+ listPendingFlowAssignmentsResponseSchema,
50
+ nullableUpdateFlowAssignmentLifecycleResponseSchema,
51
+ type ReleaseFlowAssignmentClaimRequest,
52
+ type ReleaseFlowAssignmentClaimResponse,
53
+ releaseFlowAssignmentClaimRequestSchema,
54
+ releaseFlowAssignmentClaimResponseSchema,
55
+ type UpdateFlowAssignmentLifecycleRequest,
56
+ type UpdateFlowAssignmentLifecycleResponse,
57
+ updateFlowAssignmentLifecycleRequestSchema,
58
+ updateFlowAssignmentLifecycleResponseSchema,
59
+ } from "./flow-assignments.js";
60
+ export {
61
+ buildIdentityTypeGuidanceMessage,
62
+ type CustomIdentityTypeValue,
63
+ getIdentityTypeSuggestion,
64
+ IDENTITY_TYPE_VALUES,
65
+ type IdentityTypeValue,
66
+ isIdentityTypeValue,
67
+ type StandardIdentityTypeValue,
68
+ } from "./identity-type.js";
69
+ export {
70
+ isPublicGrantScope,
71
+ type PublicGrantScope,
72
+ publicGrantScopeIdSchema,
73
+ } from "./public-grant-scope.js";
74
+ export {
75
+ type RuntimeEndpoints,
76
+ resolveApiBaseUrl,
77
+ resolveCoreUrl,
78
+ resolveTrpcBaseUrl,
79
+ runtimeEndpointsSchema,
80
+ } from "./runtime-endpoints.js";
81
+ export {
82
+ getScopeMeta,
83
+ isScope,
84
+ listScopes,
85
+ SCOPE_IDS,
86
+ type Scope,
87
+ scopeIdSchema,
88
+ } from "./scopes.js";
89
+ export {
90
+ type TrpcSuccessResponse,
91
+ trpcSuccessResponseSchema,
92
+ } from "./trpc-envelope.js";
93
+ export {
94
+ type FlowVersionResolution,
95
+ flowVersionResolutionSchema,
96
+ type ThemeVersionResolution,
97
+ themeVersionResolutionSchema,
98
+ } from "./version-resolution.js";
99
+ export {
100
+ type PublicCommandPayload,
101
+ parseConfigureOptions,
102
+ parseInitOptions,
103
+ parsePublicCommand,
104
+ publicCommandPayloadSchema,
105
+ publicWidgetCommandKindSchema,
106
+ } from "./widget-commands.js";
107
+ export {
108
+ type AuthConfigInput,
109
+ type AuthJwtConfigInput,
110
+ authConfigInputSchema,
111
+ authJwtConfigInputSchema,
112
+ type ColorSchemeConfigInput,
113
+ type ConfigureOptions,
114
+ type ConsentConfigInput,
115
+ type CoreInstanceConfig,
116
+ type CoreInstanceConfigInput,
117
+ colorSchemeConfigInputSchema,
118
+ configureOptionsSchema,
119
+ consentConfigInputSchema,
120
+ coreInstanceConfigSchema,
121
+ type DebugConfigInput,
122
+ debugInputSchema,
123
+ type FlagsConfigInput,
124
+ flagsConfigInputSchema,
125
+ type HostCapabilitiesConfigInput,
126
+ hostCapabilitiesConfigInputSchema,
127
+ type IdentityConfigInput,
128
+ INSTANCE_CONFIG_UPDATE_KEYS,
129
+ type InitOptions,
130
+ type InstanceUpdateableConfig,
131
+ identityConfigInputSchema,
132
+ initOptionsSchema,
133
+ instanceUpdateableConfigSchema,
134
+ type TelemetryConfigInput,
135
+ telemetryConfigInputSchema,
136
+ } from "./widget-config.js";
@@ -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
+ };