@frak-labs/core-sdk 1.0.0 → 1.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/cdn/bundle.js +3 -3
- package/dist/actions-BlCQVBQJ.js +1 -0
- package/dist/actions-Bwj4zSdB.cjs +1 -0
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +2 -2
- package/dist/actions.d.ts +2 -2
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -1
- package/dist/bundle.d.cts +4 -4
- package/dist/bundle.d.ts +4 -4
- package/dist/bundle.js +1 -1
- package/dist/{index-Dwmo109y.d.cts → index-9TdOc_ub.d.ts} +169 -44
- package/dist/{index-BphwTmKA.d.cts → index-BWic1g0J.d.cts} +1 -1
- package/dist/{index-BV5D9DsW.d.ts → index-DPIqLMCR.d.cts} +169 -44
- package/dist/{index-_f8EuN_1.d.ts → index-Du4nB3qO.d.ts} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/{openSso-BwEK2M98.d.cts → openSso-3YqtmSkM.d.ts} +116 -10
- package/dist/{openSso-C1Wzl5-i.d.ts → openSso-SP6T9cHA.d.cts} +115 -9
- package/dist/sdkConfigStore-BXzz5PlK.js +1 -0
- package/dist/sdkConfigStore-DDL_fjYX.cjs +1 -0
- package/dist/src-CqKED785.cjs +13 -0
- package/dist/src-u4vW9qh0.js +13 -0
- package/package.json +1 -1
- package/src/actions/referral/processReferral.test.ts +129 -8
- package/src/actions/referral/processReferral.ts +27 -17
- package/src/clients/createIFrameFrakClient.ts +84 -3
- package/src/index.ts +8 -1
- package/src/types/config.ts +9 -0
- package/src/types/context.ts +16 -4
- package/src/types/index.ts +2 -0
- package/src/types/lifecycle/client.ts +7 -0
- package/src/types/resolvedConfig.ts +10 -0
- package/src/types/rpc/displaySharingPage.ts +18 -0
- package/src/types/rpc/interaction.ts +1 -1
- package/src/types/tracking.ts +37 -1
- package/src/utils/FrakContext.test.ts +239 -9
- package/src/utils/FrakContext.ts +83 -21
- package/src/utils/analytics/events/component.ts +58 -0
- package/src/utils/analytics/events/index.ts +20 -0
- package/src/utils/analytics/events/lifecycle.ts +26 -0
- package/src/utils/analytics/events/referral.ts +11 -0
- package/src/utils/analytics/index.ts +8 -0
- package/src/utils/{trackEvent.test.ts → analytics/trackEvent.test.ts} +22 -30
- package/src/utils/analytics/trackEvent.ts +34 -0
- package/src/utils/frakContextV2Codec.test.ts +241 -0
- package/src/utils/frakContextV2Codec.ts +197 -0
- package/src/utils/index.ts +5 -1
- package/src/utils/mergeAttribution.test.ts +153 -0
- package/src/utils/mergeAttribution.ts +75 -0
- package/dist/actions-D4aBXbdp.cjs +0 -1
- package/dist/actions-Dq_uN-wn.js +0 -1
- package/dist/src-B1eliIi6.cjs +0 -13
- package/dist/src-C0UH1GsN.js +0 -13
- package/dist/trackEvent-BqJqRZ-u.cjs +0 -1
- package/dist/trackEvent-Bqq4jd6R.js +0 -1
- package/src/utils/trackEvent.ts +0 -41
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
expect,
|
|
11
11
|
it,
|
|
12
12
|
vi,
|
|
13
|
-
} from "
|
|
14
|
-
import type { FrakClient } from "
|
|
13
|
+
} from "../../../tests/vitest-fixtures";
|
|
14
|
+
import type { FrakClient } from "../../types";
|
|
15
15
|
import { trackEvent } from "./trackEvent";
|
|
16
16
|
|
|
17
17
|
describe("trackEvent", () => {
|
|
@@ -42,16 +42,19 @@ describe("trackEvent", () => {
|
|
|
42
42
|
|
|
43
43
|
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
44
44
|
"share_button_clicked",
|
|
45
|
-
|
|
45
|
+
undefined
|
|
46
46
|
);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it("should track event with props", () => {
|
|
50
|
-
const props = {
|
|
51
|
-
|
|
50
|
+
const props = {
|
|
51
|
+
placement: "footer",
|
|
52
|
+
click_action: "share-modal",
|
|
53
|
+
} as const;
|
|
54
|
+
trackEvent(mockClient, "share_button_clicked", props);
|
|
52
55
|
|
|
53
56
|
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
54
|
-
"
|
|
57
|
+
"share_button_clicked",
|
|
55
58
|
props
|
|
56
59
|
);
|
|
57
60
|
});
|
|
@@ -71,25 +74,18 @@ describe("trackEvent", () => {
|
|
|
71
74
|
|
|
72
75
|
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
73
76
|
"user_referred_started",
|
|
74
|
-
|
|
77
|
+
undefined
|
|
75
78
|
);
|
|
76
79
|
});
|
|
77
80
|
|
|
78
81
|
it("should track user_referred_completed event", () => {
|
|
79
|
-
trackEvent(mockClient, "user_referred_completed"
|
|
82
|
+
trackEvent(mockClient, "user_referred_completed", {
|
|
83
|
+
status: "success",
|
|
84
|
+
});
|
|
80
85
|
|
|
81
86
|
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
82
87
|
"user_referred_completed",
|
|
83
|
-
{}
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("should track user_referred_error event", () => {
|
|
88
|
-
trackEvent(mockClient, "user_referred_error");
|
|
89
|
-
|
|
90
|
-
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
91
|
-
"user_referred_error",
|
|
92
|
-
{}
|
|
88
|
+
{ status: "success" }
|
|
93
89
|
);
|
|
94
90
|
});
|
|
95
91
|
});
|
|
@@ -102,7 +98,7 @@ describe("trackEvent", () => {
|
|
|
102
98
|
});
|
|
103
99
|
|
|
104
100
|
it("should log debug message when client is undefined", () => {
|
|
105
|
-
trackEvent(undefined, "
|
|
101
|
+
trackEvent(undefined, "share_button_clicked");
|
|
106
102
|
|
|
107
103
|
expect(consoleDebugSpy).toHaveBeenCalledWith(
|
|
108
104
|
"[Frak] No client provided, skipping event tracking"
|
|
@@ -143,30 +139,26 @@ describe("trackEvent", () => {
|
|
|
143
139
|
const clientWithoutPanel = {} as FrakClient;
|
|
144
140
|
|
|
145
141
|
expect(() => {
|
|
146
|
-
trackEvent(clientWithoutPanel, "
|
|
142
|
+
trackEvent(clientWithoutPanel, "share_button_clicked");
|
|
147
143
|
}).not.toThrow();
|
|
148
144
|
});
|
|
149
145
|
});
|
|
150
146
|
|
|
151
147
|
describe("edge cases", () => {
|
|
152
|
-
it("should handle
|
|
153
|
-
trackEvent(mockClient, "share_button_clicked", {
|
|
148
|
+
it("should handle typed props object", () => {
|
|
149
|
+
trackEvent(mockClient, "share_button_clicked", {
|
|
150
|
+
click_action: "share-modal",
|
|
151
|
+
});
|
|
154
152
|
|
|
155
153
|
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
156
154
|
"share_button_clicked",
|
|
157
|
-
{}
|
|
155
|
+
{ click_action: "share-modal" }
|
|
158
156
|
);
|
|
159
157
|
});
|
|
160
158
|
|
|
161
159
|
it("should handle complex props object", () => {
|
|
162
160
|
const complexProps = {
|
|
163
|
-
|
|
164
|
-
metadata: {
|
|
165
|
-
page: "home",
|
|
166
|
-
section: "header",
|
|
167
|
-
},
|
|
168
|
-
tags: ["tag1", "tag2"],
|
|
169
|
-
timestamp: Date.now(),
|
|
161
|
+
status: "success" as const,
|
|
170
162
|
};
|
|
171
163
|
|
|
172
164
|
trackEvent(mockClient, "user_referred_completed", complexProps);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FrakClient } from "../../types";
|
|
2
|
+
import type { SdkEventMap } from "./events";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Track an analytics event via the SDK's OpenPanel instance.
|
|
6
|
+
* Fire-and-forget — silently catches errors so analytics never break a
|
|
7
|
+
* partner integration.
|
|
8
|
+
*
|
|
9
|
+
* The client must be passed explicitly because the OpenPanel instance is
|
|
10
|
+
* scoped to each `FrakClient` (a partner site may hold multiple iframes).
|
|
11
|
+
*
|
|
12
|
+
* @param client - The Frak client instance (no-op if undefined)
|
|
13
|
+
* @param event - Typed event name from the SDK event map
|
|
14
|
+
* @param properties - Typed properties for the given event
|
|
15
|
+
*/
|
|
16
|
+
export function trackEvent<K extends keyof SdkEventMap>(
|
|
17
|
+
client: FrakClient | undefined,
|
|
18
|
+
event: K,
|
|
19
|
+
properties?: SdkEventMap[K]
|
|
20
|
+
): void {
|
|
21
|
+
if (!client) {
|
|
22
|
+
console.debug("[Frak] No client provided, skipping event tracking");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
client.openPanel?.track(
|
|
28
|
+
event as string,
|
|
29
|
+
properties as Record<string, unknown> | undefined
|
|
30
|
+
);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.debug("[Frak] Failed to track event:", event, e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { Address } from "viem";
|
|
2
|
+
import { describe, expect, it } from "../../tests/vitest-fixtures";
|
|
3
|
+
import type { FrakContextV2 } from "../types";
|
|
4
|
+
import { base64urlEncode } from "./compression/b64";
|
|
5
|
+
import {
|
|
6
|
+
decodeFrakContextV2,
|
|
7
|
+
encodeFrakContextV2,
|
|
8
|
+
isV2BinaryLength,
|
|
9
|
+
} from "./frakContextV2Codec";
|
|
10
|
+
|
|
11
|
+
const MERCHANT = "550e8400-e29b-41d4-a716-446655440000";
|
|
12
|
+
const CLIENT = "550e8400-e29b-41d4-a716-446655440001";
|
|
13
|
+
const WALLET = "0x1234567890123456789012345678901234567890" as Address;
|
|
14
|
+
|
|
15
|
+
describe("frakContextV2Codec", () => {
|
|
16
|
+
describe("encodeFrakContextV2 / decodeFrakContextV2 round-trip", () => {
|
|
17
|
+
it("round-trips a context with clientId only (37 bytes)", () => {
|
|
18
|
+
const ctx: FrakContextV2 = {
|
|
19
|
+
v: 2,
|
|
20
|
+
m: MERCHANT,
|
|
21
|
+
t: 1709654400,
|
|
22
|
+
c: CLIENT,
|
|
23
|
+
};
|
|
24
|
+
const encoded = encodeFrakContextV2(ctx);
|
|
25
|
+
expect(encoded).toBeInstanceOf(Uint8Array);
|
|
26
|
+
expect(encoded?.length).toBe(37);
|
|
27
|
+
expect(decodeFrakContextV2(encoded as Uint8Array)).toEqual(ctx);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("round-trips a context with wallet only (41 bytes)", () => {
|
|
31
|
+
const ctx: FrakContextV2 = {
|
|
32
|
+
v: 2,
|
|
33
|
+
m: MERCHANT,
|
|
34
|
+
t: 1709654400,
|
|
35
|
+
w: WALLET,
|
|
36
|
+
};
|
|
37
|
+
const encoded = encodeFrakContextV2(ctx);
|
|
38
|
+
expect(encoded?.length).toBe(41);
|
|
39
|
+
expect(decodeFrakContextV2(encoded as Uint8Array)).toEqual(ctx);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("round-trips a context with clientId + wallet (57 bytes)", () => {
|
|
43
|
+
const ctx: FrakContextV2 = {
|
|
44
|
+
v: 2,
|
|
45
|
+
m: MERCHANT,
|
|
46
|
+
t: 1709654400,
|
|
47
|
+
c: CLIENT,
|
|
48
|
+
w: WALLET,
|
|
49
|
+
};
|
|
50
|
+
const encoded = encodeFrakContextV2(ctx);
|
|
51
|
+
expect(encoded?.length).toBe(57);
|
|
52
|
+
expect(decodeFrakContextV2(encoded as Uint8Array)).toEqual(ctx);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("produces a base64url string shorter than the legacy JSON format", () => {
|
|
56
|
+
// Legacy reference: a typical anonymous context is ~115 JSON bytes
|
|
57
|
+
// \u2192 ~154 base64url chars. Wallet variant is ~165 \u2192 ~220 chars.
|
|
58
|
+
const ctxBoth: FrakContextV2 = {
|
|
59
|
+
v: 2,
|
|
60
|
+
m: MERCHANT,
|
|
61
|
+
t: 1709654400,
|
|
62
|
+
c: CLIENT,
|
|
63
|
+
w: WALLET,
|
|
64
|
+
};
|
|
65
|
+
const encoded = base64urlEncode(
|
|
66
|
+
encodeFrakContextV2(ctxBoth) as Uint8Array
|
|
67
|
+
);
|
|
68
|
+
// 57 bytes encodes to 76 chars (no padding).
|
|
69
|
+
expect(encoded.length).toBe(76);
|
|
70
|
+
// Sanity: far below the legacy ~220-char payload.
|
|
71
|
+
expect(encoded.length).toBeLessThan(100);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("preserves UUID case insensitivity on decode", () => {
|
|
75
|
+
const ctx: FrakContextV2 = {
|
|
76
|
+
v: 2,
|
|
77
|
+
m: MERCHANT.toUpperCase(),
|
|
78
|
+
t: 1,
|
|
79
|
+
c: CLIENT,
|
|
80
|
+
};
|
|
81
|
+
const encoded = encodeFrakContextV2(ctx);
|
|
82
|
+
const decoded = decodeFrakContextV2(encoded as Uint8Array);
|
|
83
|
+
// Decoded UUIDs are lower-case canonical.
|
|
84
|
+
expect(decoded?.m).toBe(MERCHANT);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("preserves timestamp at the uint32 boundary", () => {
|
|
88
|
+
const ctx: FrakContextV2 = {
|
|
89
|
+
v: 2,
|
|
90
|
+
m: MERCHANT,
|
|
91
|
+
t: 0xff_ff_ff_ff,
|
|
92
|
+
c: CLIENT,
|
|
93
|
+
};
|
|
94
|
+
const decoded = decodeFrakContextV2(
|
|
95
|
+
encodeFrakContextV2(ctx) as Uint8Array
|
|
96
|
+
);
|
|
97
|
+
expect(decoded?.t).toBe(0xff_ff_ff_ff);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("encodeFrakContextV2 validation", () => {
|
|
102
|
+
it("rejects non-UUID merchant id", () => {
|
|
103
|
+
expect(
|
|
104
|
+
encodeFrakContextV2({
|
|
105
|
+
v: 2,
|
|
106
|
+
m: "not-a-uuid",
|
|
107
|
+
t: 1,
|
|
108
|
+
c: CLIENT,
|
|
109
|
+
})
|
|
110
|
+
).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("rejects non-UUID client id", () => {
|
|
114
|
+
expect(
|
|
115
|
+
encodeFrakContextV2({
|
|
116
|
+
v: 2,
|
|
117
|
+
m: MERCHANT,
|
|
118
|
+
t: 1,
|
|
119
|
+
c: "not-a-uuid",
|
|
120
|
+
})
|
|
121
|
+
).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("rejects malformed wallet address", () => {
|
|
125
|
+
expect(
|
|
126
|
+
encodeFrakContextV2({
|
|
127
|
+
v: 2,
|
|
128
|
+
m: MERCHANT,
|
|
129
|
+
t: 1,
|
|
130
|
+
w: "0xnot-a-wallet" as Address,
|
|
131
|
+
})
|
|
132
|
+
).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("rejects contexts missing both c and w", () => {
|
|
136
|
+
expect(
|
|
137
|
+
encodeFrakContextV2({
|
|
138
|
+
v: 2,
|
|
139
|
+
m: MERCHANT,
|
|
140
|
+
t: 1,
|
|
141
|
+
} as FrakContextV2)
|
|
142
|
+
).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("rejects timestamps outside uint32 range", () => {
|
|
146
|
+
expect(
|
|
147
|
+
encodeFrakContextV2({
|
|
148
|
+
v: 2,
|
|
149
|
+
m: MERCHANT,
|
|
150
|
+
t: -1,
|
|
151
|
+
c: CLIENT,
|
|
152
|
+
})
|
|
153
|
+
).toBeNull();
|
|
154
|
+
expect(
|
|
155
|
+
encodeFrakContextV2({
|
|
156
|
+
v: 2,
|
|
157
|
+
m: MERCHANT,
|
|
158
|
+
t: 0x1_00_00_00_00,
|
|
159
|
+
c: CLIENT,
|
|
160
|
+
})
|
|
161
|
+
).toBeNull();
|
|
162
|
+
expect(
|
|
163
|
+
encodeFrakContextV2({
|
|
164
|
+
v: 2,
|
|
165
|
+
m: MERCHANT,
|
|
166
|
+
t: 1.5,
|
|
167
|
+
c: CLIENT,
|
|
168
|
+
})
|
|
169
|
+
).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("decodeFrakContextV2 validation", () => {
|
|
174
|
+
it("returns null on wrong version nibble", () => {
|
|
175
|
+
const encoded = encodeFrakContextV2({
|
|
176
|
+
v: 2,
|
|
177
|
+
m: MERCHANT,
|
|
178
|
+
t: 1,
|
|
179
|
+
c: CLIENT,
|
|
180
|
+
}) as Uint8Array;
|
|
181
|
+
const tampered = new Uint8Array(encoded);
|
|
182
|
+
tampered[0] = (tampered[0] & 0xf0) | 0x03; // flip version to 3
|
|
183
|
+
expect(decodeFrakContextV2(tampered)).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("returns null when reserved header bits are set", () => {
|
|
187
|
+
const encoded = encodeFrakContextV2({
|
|
188
|
+
v: 2,
|
|
189
|
+
m: MERCHANT,
|
|
190
|
+
t: 1,
|
|
191
|
+
c: CLIENT,
|
|
192
|
+
}) as Uint8Array;
|
|
193
|
+
const tampered = new Uint8Array(encoded);
|
|
194
|
+
tampered[0] |= 0x80;
|
|
195
|
+
expect(decodeFrakContextV2(tampered)).toBeNull();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("returns null when neither flag is set", () => {
|
|
199
|
+
const encoded = encodeFrakContextV2({
|
|
200
|
+
v: 2,
|
|
201
|
+
m: MERCHANT,
|
|
202
|
+
t: 1,
|
|
203
|
+
c: CLIENT,
|
|
204
|
+
}) as Uint8Array;
|
|
205
|
+
const tampered = new Uint8Array(encoded);
|
|
206
|
+
tampered[0] &= 0x0f; // clear flags, keep version
|
|
207
|
+
expect(decodeFrakContextV2(tampered)).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("returns null when byte length disagrees with flags", () => {
|
|
211
|
+
const encoded = encodeFrakContextV2({
|
|
212
|
+
v: 2,
|
|
213
|
+
m: MERCHANT,
|
|
214
|
+
t: 1,
|
|
215
|
+
c: CLIENT,
|
|
216
|
+
}) as Uint8Array;
|
|
217
|
+
// Drop the trailing byte to break the expected length.
|
|
218
|
+
const truncated = encoded.subarray(0, encoded.length - 1);
|
|
219
|
+
expect(decodeFrakContextV2(truncated)).toBeNull();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("returns null on an empty buffer", () => {
|
|
223
|
+
expect(decodeFrakContextV2(new Uint8Array(0))).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("isV2BinaryLength", () => {
|
|
228
|
+
it("matches exactly the three valid V2 sizes", () => {
|
|
229
|
+
expect(isV2BinaryLength(37)).toBe(true);
|
|
230
|
+
expect(isV2BinaryLength(41)).toBe(true);
|
|
231
|
+
expect(isV2BinaryLength(57)).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("rejects V1 size and everything else", () => {
|
|
235
|
+
expect(isV2BinaryLength(20)).toBe(false);
|
|
236
|
+
expect(isV2BinaryLength(0)).toBe(false);
|
|
237
|
+
expect(isV2BinaryLength(36)).toBe(false);
|
|
238
|
+
expect(isV2BinaryLength(58)).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary codec for {@link FrakContextV2}.
|
|
3
|
+
*
|
|
4
|
+
* Produces a compact, URL-safe byte layout (~65% smaller than the previous
|
|
5
|
+
* JSON+base64url format). See the layout below.
|
|
6
|
+
*
|
|
7
|
+
* ## Wire layout
|
|
8
|
+
*
|
|
9
|
+
* ```text
|
|
10
|
+
* byte 0: header
|
|
11
|
+
* bits 0-3 version (= 2)
|
|
12
|
+
* bit 4 has_c flag
|
|
13
|
+
* bit 5 has_w flag
|
|
14
|
+
* bits 6-7 reserved (must be 0)
|
|
15
|
+
* bytes 1..16: merchant UUID (16 bytes, mandatory)
|
|
16
|
+
* bytes 17..20: timestamp (uint32 big-endian, Unix seconds)
|
|
17
|
+
* bytes 21..36: client UUID (16 bytes, only when has_c is set)
|
|
18
|
+
* bytes 37..56: wallet address (20 bytes, only when has_w is set)
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Size variants (before base64url):
|
|
22
|
+
* - has_c only: 37 bytes
|
|
23
|
+
* - has_w only: 41 bytes
|
|
24
|
+
* - has_c + has_w: 57 bytes
|
|
25
|
+
*
|
|
26
|
+
* V1 payloads are exactly 20 bytes (raw wallet address); the byte lengths never
|
|
27
|
+
* overlap, so the outer decoder can disambiguate purely on length.
|
|
28
|
+
*
|
|
29
|
+
* @ignore
|
|
30
|
+
*/
|
|
31
|
+
import { type Address, bytesToHex, hexToBytes, isAddress } from "viem";
|
|
32
|
+
import type { FrakContextV2 } from "../types";
|
|
33
|
+
|
|
34
|
+
const VERSION_V2 = 0x02;
|
|
35
|
+
const VERSION_MASK = 0x0f;
|
|
36
|
+
const FLAG_HAS_C = 1 << 4;
|
|
37
|
+
const FLAG_HAS_W = 1 << 5;
|
|
38
|
+
const RESERVED_MASK = 0xc0;
|
|
39
|
+
|
|
40
|
+
const UUID_BYTES = 16;
|
|
41
|
+
const TIMESTAMP_BYTES = 4;
|
|
42
|
+
const ADDRESS_BYTES = 20;
|
|
43
|
+
const HEADER_BYTES = 1;
|
|
44
|
+
|
|
45
|
+
const UUID_RE =
|
|
46
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
47
|
+
|
|
48
|
+
/** Strict lower-case UUID validation (RFC 4122 shape, any version/variant). */
|
|
49
|
+
function isUuid(value: unknown): value is string {
|
|
50
|
+
return typeof value === "string" && UUID_RE.test(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Parse a canonical UUID string into 16 raw bytes. */
|
|
54
|
+
function uuidToBytes(uuid: string): Uint8Array {
|
|
55
|
+
const hex = uuid.replace(/-/g, "");
|
|
56
|
+
const out = new Uint8Array(UUID_BYTES);
|
|
57
|
+
for (let i = 0; i < UUID_BYTES; i++) {
|
|
58
|
+
out[i] = Number.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Format 16 raw bytes as a canonical 8-4-4-4-12 UUID string. */
|
|
64
|
+
function bytesToUuid(bytes: Uint8Array): string {
|
|
65
|
+
let hex = "";
|
|
66
|
+
for (let i = 0; i < UUID_BYTES; i++) {
|
|
67
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
68
|
+
}
|
|
69
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Encode a {@link FrakContextV2} into its binary wire format.
|
|
74
|
+
*
|
|
75
|
+
* Returns `null` when the context fails runtime validation (missing fields,
|
|
76
|
+
* malformed UUIDs, timestamp outside uint32 range, invalid wallet).
|
|
77
|
+
*/
|
|
78
|
+
export function encodeFrakContextV2(ctx: FrakContextV2): Uint8Array | null {
|
|
79
|
+
if (!isUuid(ctx.m)) return null;
|
|
80
|
+
if (!Number.isInteger(ctx.t) || ctx.t < 0 || ctx.t > 0xff_ff_ff_ff)
|
|
81
|
+
return null;
|
|
82
|
+
|
|
83
|
+
const hasC = typeof ctx.c === "string" && ctx.c.length > 0;
|
|
84
|
+
const hasW = typeof ctx.w === "string" && isAddress(ctx.w);
|
|
85
|
+
if (!hasC && !hasW) return null;
|
|
86
|
+
if (hasC && !isUuid(ctx.c)) return null;
|
|
87
|
+
|
|
88
|
+
const size =
|
|
89
|
+
HEADER_BYTES +
|
|
90
|
+
UUID_BYTES +
|
|
91
|
+
TIMESTAMP_BYTES +
|
|
92
|
+
(hasC ? UUID_BYTES : 0) +
|
|
93
|
+
(hasW ? ADDRESS_BYTES : 0);
|
|
94
|
+
|
|
95
|
+
const buf = new Uint8Array(size);
|
|
96
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
97
|
+
let offset = 0;
|
|
98
|
+
|
|
99
|
+
buf[offset++] =
|
|
100
|
+
VERSION_V2 | (hasC ? FLAG_HAS_C : 0) | (hasW ? FLAG_HAS_W : 0);
|
|
101
|
+
|
|
102
|
+
buf.set(uuidToBytes(ctx.m), offset);
|
|
103
|
+
offset += UUID_BYTES;
|
|
104
|
+
|
|
105
|
+
view.setUint32(offset, ctx.t, false);
|
|
106
|
+
offset += TIMESTAMP_BYTES;
|
|
107
|
+
|
|
108
|
+
if (hasC) {
|
|
109
|
+
buf.set(uuidToBytes(ctx.c as string), offset);
|
|
110
|
+
offset += UUID_BYTES;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (hasW) {
|
|
114
|
+
buf.set(hexToBytes(ctx.w as Address), offset);
|
|
115
|
+
offset += ADDRESS_BYTES;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return buf;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Decode a binary {@link FrakContextV2} payload.
|
|
123
|
+
*
|
|
124
|
+
* Returns `null` when:
|
|
125
|
+
* - the header version nibble is not V2
|
|
126
|
+
* - reserved header bits are set (guards against future-version payloads)
|
|
127
|
+
* - neither flag is set (invalid: V2 must carry `c` and/or `w`)
|
|
128
|
+
* - the byte length does not match the length implied by the header flags
|
|
129
|
+
* - the decoded wallet does not pass `isAddress` (defense-in-depth against
|
|
130
|
+
* crafted payloads that round-trip by length but carry junk)
|
|
131
|
+
*/
|
|
132
|
+
export function decodeFrakContextV2(buf: Uint8Array): FrakContextV2 | null {
|
|
133
|
+
if (buf.length < HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES) return null;
|
|
134
|
+
|
|
135
|
+
const header = buf[0];
|
|
136
|
+
if ((header & VERSION_MASK) !== VERSION_V2) return null;
|
|
137
|
+
if ((header & RESERVED_MASK) !== 0) return null;
|
|
138
|
+
|
|
139
|
+
const hasC = (header & FLAG_HAS_C) !== 0;
|
|
140
|
+
const hasW = (header & FLAG_HAS_W) !== 0;
|
|
141
|
+
if (!hasC && !hasW) return null;
|
|
142
|
+
|
|
143
|
+
const expected =
|
|
144
|
+
HEADER_BYTES +
|
|
145
|
+
UUID_BYTES +
|
|
146
|
+
TIMESTAMP_BYTES +
|
|
147
|
+
(hasC ? UUID_BYTES : 0) +
|
|
148
|
+
(hasW ? ADDRESS_BYTES : 0);
|
|
149
|
+
if (buf.length !== expected) return null;
|
|
150
|
+
|
|
151
|
+
let offset = HEADER_BYTES;
|
|
152
|
+
|
|
153
|
+
const m = bytesToUuid(buf.subarray(offset, offset + UUID_BYTES));
|
|
154
|
+
offset += UUID_BYTES;
|
|
155
|
+
|
|
156
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
157
|
+
const t = view.getUint32(offset, false);
|
|
158
|
+
offset += TIMESTAMP_BYTES;
|
|
159
|
+
|
|
160
|
+
const out: FrakContextV2 = { v: 2, m, t };
|
|
161
|
+
|
|
162
|
+
if (hasC) {
|
|
163
|
+
out.c = bytesToUuid(buf.subarray(offset, offset + UUID_BYTES));
|
|
164
|
+
offset += UUID_BYTES;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (hasW) {
|
|
168
|
+
const walletHex = bytesToHex(
|
|
169
|
+
buf.subarray(offset, offset + ADDRESS_BYTES),
|
|
170
|
+
{ size: ADDRESS_BYTES }
|
|
171
|
+
) as Address;
|
|
172
|
+
if (!isAddress(walletHex)) return null;
|
|
173
|
+
out.w = walletHex;
|
|
174
|
+
offset += ADDRESS_BYTES;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Quick length-based probe to tell V1 (20-byte wallet address) apart from a V2
|
|
182
|
+
* binary payload. Exposed so the outer decoder can branch without re-parsing.
|
|
183
|
+
*/
|
|
184
|
+
export function isV2BinaryLength(byteLength: number): boolean {
|
|
185
|
+
return (
|
|
186
|
+
byteLength ===
|
|
187
|
+
HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES + UUID_BYTES ||
|
|
188
|
+
byteLength ===
|
|
189
|
+
HEADER_BYTES + UUID_BYTES + TIMESTAMP_BYTES + ADDRESS_BYTES ||
|
|
190
|
+
byteLength ===
|
|
191
|
+
HEADER_BYTES +
|
|
192
|
+
UUID_BYTES +
|
|
193
|
+
TIMESTAMP_BYTES +
|
|
194
|
+
UUID_BYTES +
|
|
195
|
+
ADDRESS_BYTES
|
|
196
|
+
);
|
|
197
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Deferred } from "@frak-labs/frame-connector";
|
|
2
|
+
export { trackEvent } from "./analytics";
|
|
2
3
|
export { getBackendUrl } from "./backendUrl";
|
|
3
4
|
export { clearAllCache, getCache, withCache } from "./cache";
|
|
4
5
|
export { getClientId } from "./clientId";
|
|
@@ -28,6 +29,10 @@ export {
|
|
|
28
29
|
isIOS,
|
|
29
30
|
redirectToExternalBrowser,
|
|
30
31
|
} from "./inAppBrowser";
|
|
32
|
+
export {
|
|
33
|
+
type MergeAttributionInput,
|
|
34
|
+
mergeAttribution,
|
|
35
|
+
} from "./mergeAttribution";
|
|
31
36
|
export { sdkConfigStore } from "./sdkConfigStore";
|
|
32
37
|
export {
|
|
33
38
|
type AppSpecificSsoMetadata,
|
|
@@ -35,4 +40,3 @@ export {
|
|
|
35
40
|
type FullSsoParams,
|
|
36
41
|
generateSsoUrl,
|
|
37
42
|
} from "./sso";
|
|
38
|
-
export { type FrakEvent, trackEvent } from "./trackEvent";
|