@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.
- package/dist/app-event.d.ts +659 -17
- package/dist/app-event.d.ts.map +1 -1
- package/dist/app-event.js +71 -8
- package/dist/errors.d.ts +73 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +43 -0
- package/dist/flow-assignments.d.ts +83 -4
- package/dist/flow-assignments.d.ts.map +1 -1
- package/dist/flow-assignments.js +1 -0
- package/dist/host/constants.d.ts +2 -2
- package/dist/host/constants.d.ts.map +1 -1
- package/dist/host/constants.js +3 -1
- package/dist/host/sdk-types.d.ts +73 -28
- package/dist/host/sdk-types.d.ts.map +1 -1
- package/dist/host/sdk.d.ts.map +1 -1
- package/dist/identity-type.d.ts +4 -2
- package/dist/identity-type.d.ts.map +1 -1
- package/dist/identity-type.js +4 -2
- package/dist/index.d.ts +4 -86
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -6
- package/dist/widget-commands.d.ts +102 -3
- package/dist/widget-commands.d.ts.map +1 -1
- package/dist/widget-commands.js +5 -0
- package/dist/widget-config.d.ts +5 -5
- package/package.json +22 -2
- package/src/app-event.ts +211 -0
- package/src/client-meta.ts +25 -0
- package/src/defaults.ts +4 -0
- package/src/errors.ts +58 -0
- package/src/flow-assignments.test.ts +63 -0
- package/src/flow-assignments.ts +125 -0
- package/src/host/command-dispatch.ts +59 -0
- package/src/host/command-envelope.ts +41 -0
- package/src/host/command-settlement.ts +121 -0
- package/src/host/constants.ts +103 -0
- package/src/host/host-event-contract.ts +277 -0
- package/src/host/index.ts +13 -0
- package/src/host/lazy-handle-client.ts +207 -0
- package/src/host/request-id.ts +3 -0
- package/src/host/sdk-types.ts +506 -0
- package/src/host/sdk.ts +43 -0
- package/src/host/unique-id.ts +17 -0
- package/src/identity-type.ts +102 -0
- package/src/index.ts +136 -0
- package/src/protocol-root.test.ts +476 -0
- package/src/public-grant-scope.ts +25 -0
- package/src/runtime-endpoints.ts +69 -0
- package/src/scopes.ts +79 -0
- package/src/trpc-envelope.ts +9 -0
- package/src/version-resolution.ts +18 -0
- package/src/widget-commands.ts +152 -0
- package/src/widget-config.ts +157 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
claimPendingFlowAssignmentsRequestSchema,
|
|
4
|
+
flowAssignmentRecipientRequestSchema,
|
|
5
|
+
updateFlowAssignmentLifecycleRequestSchema,
|
|
6
|
+
} from "./flow-assignments";
|
|
7
|
+
|
|
8
|
+
describe("flow assignment request contracts", () => {
|
|
9
|
+
it("accepts runtime targeting context for listPending", () => {
|
|
10
|
+
const result = flowAssignmentRecipientRequestSchema.parse({
|
|
11
|
+
identities: [{ type: "userId", value: "user_1" }],
|
|
12
|
+
context: {
|
|
13
|
+
flags: [{ key: "new_checkout", value: true }],
|
|
14
|
+
capabilities: [{ key: "checkout.drawer", source: "host-app" }],
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(result.context?.flags?.[0]).toEqual({
|
|
19
|
+
key: "new_checkout",
|
|
20
|
+
value: true,
|
|
21
|
+
});
|
|
22
|
+
expect(result.context?.capabilities?.[0]).toEqual({
|
|
23
|
+
key: "checkout.drawer",
|
|
24
|
+
source: "host-app",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("accepts runtime targeting context for claimPending", () => {
|
|
29
|
+
const result = claimPendingFlowAssignmentsRequestSchema.parse({
|
|
30
|
+
holdOwnerKey: "inst_1",
|
|
31
|
+
context: {
|
|
32
|
+
flags: [{ key: "pricing_test", value: "variant_a" }],
|
|
33
|
+
capabilities: [{ key: "billing.portal" }],
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(result.identities).toEqual([]);
|
|
38
|
+
expect(result.context?.flags?.[0]?.key).toBe("pricing_test");
|
|
39
|
+
expect(result.context?.capabilities?.[0]?.key).toBe("billing.portal");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("accepts runtime targeting context for lifecycle updates", () => {
|
|
43
|
+
const result = updateFlowAssignmentLifecycleRequestSchema.parse({
|
|
44
|
+
identities: [{ type: "userId", value: "user_1" }],
|
|
45
|
+
flowAssignmentId: "flasn_1",
|
|
46
|
+
flowVersionId: "flv_1",
|
|
47
|
+
holdGroupToken: "fahold_1",
|
|
48
|
+
context: {
|
|
49
|
+
flags: [{ key: "new_checkout", value: true }],
|
|
50
|
+
capabilities: [{ key: "checkout.drawer", source: "host-app" }],
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
expect(result.context?.flags?.[0]).toEqual({
|
|
55
|
+
key: "new_checkout",
|
|
56
|
+
value: true,
|
|
57
|
+
});
|
|
58
|
+
expect(result.context?.capabilities?.[0]).toEqual({
|
|
59
|
+
key: "checkout.drawer",
|
|
60
|
+
source: "host-app",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as z from "zod/mini";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
appEventCapabilitySchema,
|
|
5
|
+
appEventFlagSchema,
|
|
6
|
+
appEventIdentitySchema,
|
|
7
|
+
} from "./app-event.js";
|
|
8
|
+
|
|
9
|
+
export const flowAssignmentStatusSchema = z.enum([
|
|
10
|
+
"pending",
|
|
11
|
+
"held",
|
|
12
|
+
"presented",
|
|
13
|
+
"completed",
|
|
14
|
+
"expired",
|
|
15
|
+
"superseded",
|
|
16
|
+
"withdrawn_pending",
|
|
17
|
+
"withdrawn_presented",
|
|
18
|
+
"withdrawn_final",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const flowAssignmentTargetingContextSchema = z.object({
|
|
22
|
+
flags: z.optional(z.array(appEventFlagSchema)),
|
|
23
|
+
capabilities: z.optional(z.array(appEventCapabilitySchema)),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const flowAssignmentRecipientRequestSchema = z.object({
|
|
27
|
+
identities: z._default(z.array(appEventIdentitySchema), []),
|
|
28
|
+
context: z.optional(flowAssignmentTargetingContextSchema),
|
|
29
|
+
at: z.optional(z.number()),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const flowAssignmentRecordSchema = z.object({
|
|
33
|
+
flowAssignmentId: z.string().check(z.trim(), z.minLength(1)),
|
|
34
|
+
flowId: z.string().check(z.trim(), z.minLength(1)),
|
|
35
|
+
holdGroupToken: z.optional(z.string().check(z.trim(), z.minLength(1))),
|
|
36
|
+
presentedFlowVersionId: z.optional(
|
|
37
|
+
z.string().check(z.trim(), z.minLength(1)),
|
|
38
|
+
),
|
|
39
|
+
latestFlowVersionId: z.string().check(z.trim(), z.minLength(1)),
|
|
40
|
+
status: flowAssignmentStatusSchema,
|
|
41
|
+
assignedAt: z.string(),
|
|
42
|
+
expiresAt: z.optional(z.string()),
|
|
43
|
+
heldAt: z.optional(z.string()),
|
|
44
|
+
holdExpiresAt: z.optional(z.string()),
|
|
45
|
+
presentedAt: z.optional(z.string()),
|
|
46
|
+
completedAt: z.optional(z.string()),
|
|
47
|
+
expiredAt: z.optional(z.string()),
|
|
48
|
+
supersededAt: z.optional(z.string()),
|
|
49
|
+
withdrawnAt: z.optional(z.string()),
|
|
50
|
+
withdrawnFinalAt: z.optional(z.string()),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const listPendingFlowAssignmentsResponseSchema = z.object({
|
|
54
|
+
fetchedAt: z.string(),
|
|
55
|
+
flowAssignments: z.array(flowAssignmentRecordSchema),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const claimPendingFlowAssignmentsRequestSchema = z.object({
|
|
59
|
+
identities: z._default(z.array(appEventIdentitySchema), []),
|
|
60
|
+
context: z.optional(flowAssignmentTargetingContextSchema),
|
|
61
|
+
holdOwnerKey: z.string().check(z.trim(), z.minLength(1)),
|
|
62
|
+
at: z.optional(z.number()),
|
|
63
|
+
limit: z.optional(z.number()),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const claimPendingFlowAssignmentsResponseSchema = z.object({
|
|
67
|
+
fetchedAt: z.string(),
|
|
68
|
+
flowAssignments: z.array(flowAssignmentRecordSchema),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const updateFlowAssignmentLifecycleRequestSchema = z.object({
|
|
72
|
+
identities: z._default(z.array(appEventIdentitySchema), []),
|
|
73
|
+
context: z.optional(flowAssignmentTargetingContextSchema),
|
|
74
|
+
flowAssignmentId: z.string().check(z.trim(), z.minLength(1)),
|
|
75
|
+
flowVersionId: z.optional(z.string().check(z.trim(), z.minLength(1))),
|
|
76
|
+
holdGroupToken: z.optional(z.string().check(z.trim(), z.minLength(1))),
|
|
77
|
+
at: z.optional(z.number()),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const releaseFlowAssignmentClaimRequestSchema = z.object({
|
|
81
|
+
identities: z._default(z.array(appEventIdentitySchema), []),
|
|
82
|
+
holdGroupToken: z.string().check(z.trim(), z.minLength(1)),
|
|
83
|
+
holdOwnerKey: z.optional(z.string().check(z.trim(), z.minLength(1))),
|
|
84
|
+
at: z.optional(z.number()),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const releaseFlowAssignmentClaimResponseSchema = z.object({
|
|
88
|
+
releasedCount: z.number(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const updateFlowAssignmentLifecycleResponseSchema =
|
|
92
|
+
flowAssignmentRecordSchema;
|
|
93
|
+
export const nullableUpdateFlowAssignmentLifecycleResponseSchema = z.nullable(
|
|
94
|
+
updateFlowAssignmentLifecycleResponseSchema,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
export type FlowAssignmentStatus = z.output<typeof flowAssignmentStatusSchema>;
|
|
98
|
+
export type FlowAssignmentRecipientRequest = z.output<
|
|
99
|
+
typeof flowAssignmentRecipientRequestSchema
|
|
100
|
+
>;
|
|
101
|
+
export type FlowAssignmentTargetingContext = z.output<
|
|
102
|
+
typeof flowAssignmentTargetingContextSchema
|
|
103
|
+
>;
|
|
104
|
+
export type FlowAssignmentRecord = z.output<typeof flowAssignmentRecordSchema>;
|
|
105
|
+
export type ClaimPendingFlowAssignmentsRequest = z.output<
|
|
106
|
+
typeof claimPendingFlowAssignmentsRequestSchema
|
|
107
|
+
>;
|
|
108
|
+
export type ClaimPendingFlowAssignmentsResponse = z.output<
|
|
109
|
+
typeof claimPendingFlowAssignmentsResponseSchema
|
|
110
|
+
>;
|
|
111
|
+
export type ListPendingFlowAssignmentsResponse = z.output<
|
|
112
|
+
typeof listPendingFlowAssignmentsResponseSchema
|
|
113
|
+
>;
|
|
114
|
+
export type ReleaseFlowAssignmentClaimRequest = z.output<
|
|
115
|
+
typeof releaseFlowAssignmentClaimRequestSchema
|
|
116
|
+
>;
|
|
117
|
+
export type ReleaseFlowAssignmentClaimResponse = z.output<
|
|
118
|
+
typeof releaseFlowAssignmentClaimResponseSchema
|
|
119
|
+
>;
|
|
120
|
+
export type UpdateFlowAssignmentLifecycleRequest = z.output<
|
|
121
|
+
typeof updateFlowAssignmentLifecycleRequestSchema
|
|
122
|
+
>;
|
|
123
|
+
export type UpdateFlowAssignmentLifecycleResponse = z.output<
|
|
124
|
+
typeof updateFlowAssignmentLifecycleResponseSchema
|
|
125
|
+
>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createCommandEnvelope } from "./command-envelope.js";
|
|
2
|
+
import { waitForCommandSettlement } from "./command-settlement.js";
|
|
3
|
+
import type { CommandEnvelope, PublicCommandPayload } from "./sdk-types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A command input: the pure payload with `kind` and command-specific fields.
|
|
7
|
+
* Distributes over the PublicCommandPayload union so discriminant
|
|
8
|
+
* narrowing is preserved (e.g. `{ kind: "open", flowId }` type-checks).
|
|
9
|
+
*/
|
|
10
|
+
export type CommandInput = PublicCommandPayload extends infer C
|
|
11
|
+
? C extends PublicCommandPayload
|
|
12
|
+
? C
|
|
13
|
+
: never
|
|
14
|
+
: never;
|
|
15
|
+
|
|
16
|
+
type DispatchOptions = {
|
|
17
|
+
idempotencyKey?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a dispatch function that wraps a command payload in an envelope,
|
|
22
|
+
* pushes it into the queue, and returns a promise that settles when the
|
|
23
|
+
* runtime acknowledges it.
|
|
24
|
+
*
|
|
25
|
+
* Request IDs are generated automatically.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const dispatch = createCommandDispatch((env) => queue.push(env));
|
|
30
|
+
* await dispatch({ kind: "open", flowId: "f1", flowHandleId: "h1" });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const createCommandDispatch = (
|
|
34
|
+
push: (envelope: CommandEnvelope) => void,
|
|
35
|
+
): ((input: CommandInput, options?: DispatchOptions) => Promise<void>) => {
|
|
36
|
+
return async (
|
|
37
|
+
input: CommandInput,
|
|
38
|
+
options?: DispatchOptions,
|
|
39
|
+
): Promise<void> => {
|
|
40
|
+
const envelope: CommandEnvelope = createCommandEnvelope({
|
|
41
|
+
command: input,
|
|
42
|
+
idempotencyKey:
|
|
43
|
+
resolveIdempotencyKey(options?.idempotencyKey) ?? undefined,
|
|
44
|
+
});
|
|
45
|
+
push(envelope);
|
|
46
|
+
await waitForCommandSettlement({ requestId: envelope.requestId });
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const resolveIdempotencyKey = (value?: string): string | null => {
|
|
51
|
+
if (value === undefined) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const idempotencyKey = value.trim();
|
|
55
|
+
if (!idempotencyKey) {
|
|
56
|
+
throw new Error("idempotencyKey must be a non-empty string");
|
|
57
|
+
}
|
|
58
|
+
return idempotencyKey;
|
|
59
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createCommandRequestId } from "./request-id.js";
|
|
2
|
+
import type {
|
|
3
|
+
ClientMeta,
|
|
4
|
+
CommandEnvelope,
|
|
5
|
+
PublicCommandPayload,
|
|
6
|
+
} from "./sdk-types.js";
|
|
7
|
+
|
|
8
|
+
export type CreateCommandEnvelopeInput = {
|
|
9
|
+
command: PublicCommandPayload;
|
|
10
|
+
requestId?: string | undefined;
|
|
11
|
+
idempotencyKey?: string | undefined;
|
|
12
|
+
instanceId?: string | undefined;
|
|
13
|
+
clientMeta?: ClientMeta | undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const normalizeOptionalString = (value?: string): string | undefined => {
|
|
17
|
+
if (value === undefined) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const normalized = value.trim();
|
|
21
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const createCommandEnvelope = (
|
|
25
|
+
input: CreateCommandEnvelopeInput,
|
|
26
|
+
): CommandEnvelope => {
|
|
27
|
+
const requestId =
|
|
28
|
+
normalizeOptionalString(input.requestId) ?? createCommandRequestId();
|
|
29
|
+
const idempotencyKey =
|
|
30
|
+
normalizeOptionalString(input.idempotencyKey) ?? requestId;
|
|
31
|
+
const instanceId = normalizeOptionalString(input.instanceId);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
version: "1",
|
|
35
|
+
command: input.command,
|
|
36
|
+
requestId,
|
|
37
|
+
idempotencyKey,
|
|
38
|
+
...(instanceId !== undefined ? { instanceId } : {}),
|
|
39
|
+
...(input.clientMeta !== undefined ? { clientMeta: input.clientMeta } : {}),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { HOST_EVENT_KEYS, toHostEventName } from "./constants.js";
|
|
2
|
+
import {
|
|
3
|
+
type CommandSettledDetail,
|
|
4
|
+
isCommandSettledDetail,
|
|
5
|
+
} from "./host-event-contract.js";
|
|
6
|
+
|
|
7
|
+
type WaitForCommandSettlementInput = {
|
|
8
|
+
requestId: string;
|
|
9
|
+
windowRef?: Window;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const DEFAULT_SETTLEMENT_TIMEOUT_MS = 30_000;
|
|
14
|
+
export const COMMAND_SETTLEMENT_TIMEOUT_CODE =
|
|
15
|
+
"COMMAND_SETTLEMENT_TIMEOUT" as const;
|
|
16
|
+
|
|
17
|
+
export class CommandSettlementError extends Error {
|
|
18
|
+
readonly code?: string;
|
|
19
|
+
readonly requestId: string;
|
|
20
|
+
readonly kind: string;
|
|
21
|
+
readonly instanceId: string | null;
|
|
22
|
+
|
|
23
|
+
constructor(input: {
|
|
24
|
+
requestId: string;
|
|
25
|
+
kind: string;
|
|
26
|
+
instanceId: string | null;
|
|
27
|
+
message: string;
|
|
28
|
+
code?: string;
|
|
29
|
+
}) {
|
|
30
|
+
super(input.message);
|
|
31
|
+
this.name = "CommandSettlementError";
|
|
32
|
+
this.requestId = input.requestId;
|
|
33
|
+
this.kind = input.kind;
|
|
34
|
+
this.instanceId = input.instanceId;
|
|
35
|
+
if (input.code) {
|
|
36
|
+
this.code = input.code;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type CommandSettledFailureDetail = Extract<CommandSettledDetail, { ok: false }>;
|
|
42
|
+
|
|
43
|
+
export const toCommandSettlementError = (
|
|
44
|
+
detail: CommandSettledFailureDetail,
|
|
45
|
+
): CommandSettlementError => {
|
|
46
|
+
return new CommandSettlementError({
|
|
47
|
+
requestId: detail.requestId,
|
|
48
|
+
kind: detail.kind,
|
|
49
|
+
instanceId: detail.instanceId,
|
|
50
|
+
message:
|
|
51
|
+
detail.error.message ??
|
|
52
|
+
`Command ${detail.kind} failed without an error payload`,
|
|
53
|
+
code: detail.error.code,
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export class CommandSettlementTimeoutError extends Error {
|
|
58
|
+
readonly code = COMMAND_SETTLEMENT_TIMEOUT_CODE;
|
|
59
|
+
readonly requestId: string;
|
|
60
|
+
|
|
61
|
+
constructor(requestId: string) {
|
|
62
|
+
super(`Timed out waiting for command settlement: ${requestId}`);
|
|
63
|
+
this.name = "CommandSettlementTimeoutError";
|
|
64
|
+
this.requestId = requestId;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const eventHasDetail = (event: Event): event is Event & { detail: unknown } =>
|
|
69
|
+
typeof event === "object" && event !== null && "detail" in event;
|
|
70
|
+
|
|
71
|
+
export const waitForCommandSettlement = (
|
|
72
|
+
input: WaitForCommandSettlementInput,
|
|
73
|
+
): Promise<CommandSettledDetail> => {
|
|
74
|
+
const requestId = input.requestId;
|
|
75
|
+
if (!requestId) {
|
|
76
|
+
return Promise.reject(new Error("requestId must be a non-empty string"));
|
|
77
|
+
}
|
|
78
|
+
const targetWindow =
|
|
79
|
+
input.windowRef ?? (typeof window === "undefined" ? undefined : window);
|
|
80
|
+
if (!targetWindow) {
|
|
81
|
+
return Promise.reject(
|
|
82
|
+
new Error("Cannot await command settlement without window"),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const eventName = toHostEventName(HOST_EVENT_KEYS.INSTANCE_COMMAND_SETTLED);
|
|
86
|
+
const timeoutMs = Math.max(
|
|
87
|
+
0,
|
|
88
|
+
input.timeoutMs ?? DEFAULT_SETTLEMENT_TIMEOUT_MS,
|
|
89
|
+
);
|
|
90
|
+
return new Promise<CommandSettledDetail>((resolve, reject) => {
|
|
91
|
+
const cleanup = (): void => {
|
|
92
|
+
if (typeof targetWindow.removeEventListener === "function") {
|
|
93
|
+
targetWindow.removeEventListener(eventName, handleEvent);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const timeoutId = setTimeout(() => {
|
|
98
|
+
cleanup();
|
|
99
|
+
reject(new CommandSettlementTimeoutError(requestId));
|
|
100
|
+
}, timeoutMs);
|
|
101
|
+
|
|
102
|
+
const handleEvent = (event: Event): void => {
|
|
103
|
+
if (!eventHasDetail(event)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const raw = event.detail;
|
|
107
|
+
if (!isCommandSettledDetail(raw) || raw.requestId !== requestId) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const detail = raw;
|
|
111
|
+
cleanup();
|
|
112
|
+
clearTimeout(timeoutId);
|
|
113
|
+
if (detail.ok) {
|
|
114
|
+
resolve(detail);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
reject(toCommandSettlementError(detail));
|
|
118
|
+
};
|
|
119
|
+
targetWindow.addEventListener(eventName, handleEvent);
|
|
120
|
+
});
|
|
121
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { PublicCommandPayload } from "./sdk-types.js";
|
|
2
|
+
|
|
3
|
+
export const TELEMETRY_EVENTS = {
|
|
4
|
+
INSTANCE_READY: "instance_ready",
|
|
5
|
+
VIEW_DISPLAYED: "view_displayed",
|
|
6
|
+
VIEW_CLOSED: "view_closed",
|
|
7
|
+
FLOW_DISPLAYED: "flow_displayed",
|
|
8
|
+
FLOW_CLOSED: "flow_closed",
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export type TelemetryEventName =
|
|
12
|
+
(typeof TELEMETRY_EVENTS)[keyof typeof TELEMETRY_EVENTS];
|
|
13
|
+
|
|
14
|
+
// Data attribute used to anchor widget styles & root element. Shared so build
|
|
15
|
+
// scripts, runtime code and tests stay in sync.
|
|
16
|
+
export const WIDGET_SELECTOR = "[data-gx-widget]" as const;
|
|
17
|
+
|
|
18
|
+
export const PARENT_ORIGIN_PARAM = "gx_parent_origin" as const;
|
|
19
|
+
|
|
20
|
+
// Host event names (DOM events emitted by widget for external consumption)
|
|
21
|
+
// Uses prefix "getuserfeedback:" for namespacing
|
|
22
|
+
export const HOST_EVENT_PREFIX = "getuserfeedback:" as const;
|
|
23
|
+
|
|
24
|
+
export const HOST_EVENT_KEYS = {
|
|
25
|
+
INSTANCE_FLOW_STATE_CHANGED: "instance:flow:state-changed",
|
|
26
|
+
INSTANCE_FLOW_STATE_CHANGED_INSTANCE: "instance:flow:state-changed:instance",
|
|
27
|
+
INSTANCE_COMMAND_SETTLED: "instance:command:settled",
|
|
28
|
+
INSTANCE_HANDLE_INVALIDATED: "instance:handle:invalidated",
|
|
29
|
+
INSTANCE_OPEN_REQUESTED: "instance:open:requested",
|
|
30
|
+
INSTANCE_COMMAND_UNSUPPORTED: "instance:command:unsupported",
|
|
31
|
+
INSTANCE_READY: "instance:ready",
|
|
32
|
+
INSTANCE_BRIDGE_MOUNTED: "instance:bridge:mounted",
|
|
33
|
+
INSTANCE_BRIDGE_UNMOUNTED: "instance:bridge:unmounted",
|
|
34
|
+
INSTANCE_IFRAME_READY: "instance:iframe:ready",
|
|
35
|
+
INSTANCE_ERROR: "instance:error",
|
|
36
|
+
INSTANCE_APP_EVENT: "instance:app-event",
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
type SdkCapabilityCommandKind = Exclude<PublicCommandPayload["kind"], "init">;
|
|
40
|
+
|
|
41
|
+
const SDK_COMMAND_CAPABILITY_BY_KIND = {
|
|
42
|
+
configure: "gx.command.configure.v1",
|
|
43
|
+
open: "gx.command.open.v1",
|
|
44
|
+
prefetch: "gx.command.prefetch.v1",
|
|
45
|
+
prerender: "gx.command.prerender.v1",
|
|
46
|
+
identify: "gx.command.identify.v1",
|
|
47
|
+
setContainer: "gx.command.setContainer.v1",
|
|
48
|
+
close: "gx.command.close.v1",
|
|
49
|
+
reset: "gx.command.reset.v1",
|
|
50
|
+
setDefaultContainerPolicy: "gx.command.setDefaultContainerPolicy.v1",
|
|
51
|
+
track: "gx.command.track.v1",
|
|
52
|
+
} as const satisfies Record<
|
|
53
|
+
SdkCapabilityCommandKind,
|
|
54
|
+
`gx.command.${string}.v${number}`
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
const SDK_COMMAND_CAPABILITIES = [
|
|
58
|
+
SDK_COMMAND_CAPABILITY_BY_KIND.configure,
|
|
59
|
+
SDK_COMMAND_CAPABILITY_BY_KIND.open,
|
|
60
|
+
SDK_COMMAND_CAPABILITY_BY_KIND.prefetch,
|
|
61
|
+
SDK_COMMAND_CAPABILITY_BY_KIND.prerender,
|
|
62
|
+
SDK_COMMAND_CAPABILITY_BY_KIND.identify,
|
|
63
|
+
SDK_COMMAND_CAPABILITY_BY_KIND.setContainer,
|
|
64
|
+
SDK_COMMAND_CAPABILITY_BY_KIND.close,
|
|
65
|
+
SDK_COMMAND_CAPABILITY_BY_KIND.reset,
|
|
66
|
+
SDK_COMMAND_CAPABILITY_BY_KIND.setDefaultContainerPolicy,
|
|
67
|
+
SDK_COMMAND_CAPABILITY_BY_KIND.track,
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
type SdkCommandCapability =
|
|
71
|
+
(typeof SDK_COMMAND_CAPABILITY_BY_KIND)[keyof typeof SDK_COMMAND_CAPABILITY_BY_KIND];
|
|
72
|
+
type _AssertAllCommandCapabilitiesAreAnnounced =
|
|
73
|
+
Exclude<
|
|
74
|
+
SdkCommandCapability,
|
|
75
|
+
(typeof SDK_COMMAND_CAPABILITIES)[number]
|
|
76
|
+
> extends never
|
|
77
|
+
? true
|
|
78
|
+
: never;
|
|
79
|
+
type _AssertNoUnknownCommandCapabilities =
|
|
80
|
+
Exclude<
|
|
81
|
+
(typeof SDK_COMMAND_CAPABILITIES)[number],
|
|
82
|
+
SdkCommandCapability
|
|
83
|
+
> extends never
|
|
84
|
+
? true
|
|
85
|
+
: never;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Stable capability tokens announced by the browser SDK during init.
|
|
89
|
+
* These are versioned and namespaced to support forward-compatible negotiation.
|
|
90
|
+
*/
|
|
91
|
+
export const SDK_PROTOCOL_CAPABILITIES = [
|
|
92
|
+
"gx.protocol.public.v1",
|
|
93
|
+
...SDK_COMMAND_CAPABILITIES,
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
export type SdkProtocolCapability = (typeof SDK_PROTOCOL_CAPABILITIES)[number];
|
|
97
|
+
|
|
98
|
+
export const LOADER_RUNTIME_GLOBAL_KEY = "__getuserfeedback_runtime" as const;
|
|
99
|
+
export const LOADER_RUNTIME_PROTOCOL_VERSION = "v2" as const;
|
|
100
|
+
|
|
101
|
+
// Helper to construct full event name with prefix
|
|
102
|
+
export const toHostEventName = (key: string): string =>
|
|
103
|
+
`${HOST_EVENT_PREFIX}${key}`;
|