@usequeek/app-sdk 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -16
- package/dist/app-auth.d.ts +23 -2
- package/dist/app-auth.d.ts.map +1 -1
- package/dist/app-auth.js +40 -6
- package/dist/app-auth.js.map +1 -1
- package/dist/auth-fetch.d.ts +90 -0
- package/dist/auth-fetch.d.ts.map +1 -0
- package/dist/auth-fetch.js +261 -0
- package/dist/auth-fetch.js.map +1 -0
- package/dist/client.d.ts +32 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +6 -2
- package/dist/client.js.map +1 -1
- package/dist/frame.d.ts +233 -4
- package/dist/frame.d.ts.map +1 -1
- package/dist/frame.js +694 -19
- package/dist/frame.js.map +1 -1
- package/dist/handoff.d.ts +32 -2
- package/dist/handoff.d.ts.map +1 -1
- package/dist/handoff.js +2 -1
- package/dist/handoff.js.map +1 -1
- package/dist/hono.d.ts +13 -0
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js +15 -0
- package/dist/hono.js.map +1 -1
- package/dist/index.d.ts +11 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/install-handlers.d.ts +33 -8
- package/dist/install-handlers.d.ts.map +1 -1
- package/dist/install-handlers.js +133 -15
- package/dist/install-handlers.js.map +1 -1
- package/dist/merchant-schema.d.ts +1550 -68
- package/dist/merchant-schema.d.ts.map +1 -1
- package/dist/merchant-schema.js +1 -1
- package/dist/proxy.d.ts +122 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +204 -0
- package/dist/proxy.js.map +1 -0
- package/dist/react.d.ts +78 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +262 -0
- package/dist/react.js.map +1 -0
- package/dist/scopes.d.ts +174 -0
- package/dist/scopes.d.ts.map +1 -0
- package/dist/scopes.js +297 -0
- package/dist/scopes.js.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +52 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +99 -12
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +33 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +114 -4
- package/dist/store.js.map +1 -1
- package/dist/theme.d.ts +96 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +158 -0
- package/dist/theme.js.map +1 -0
- package/dist/tokens.d.ts +30 -15
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +57 -19
- package/dist/tokens.js.map +1 -1
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +1 -1
- package/dist/webhooks.js.map +1 -1
- package/package.json +17 -5
- package/CHANGELOG.md +0 -150
- package/openapi/merchant.json +0 -21452
package/dist/frame.js
CHANGED
|
@@ -1,56 +1,731 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Framed-page
|
|
2
|
+
* Framed-page bridge client, v1 (U2): the browser-safe half of the
|
|
3
3
|
* dashboard bridge. Carries NO secret and performs NO verification — it only
|
|
4
4
|
* shapes and addresses messages. The dashboard (queek-merchant) enforces
|
|
5
5
|
* origin+source binding and mints tokens; the app backend verifies them via
|
|
6
6
|
* `./server` (`verifySessionToken`).
|
|
7
7
|
*
|
|
8
|
+
* Byte-compatible with today's protocol (`ready`/`token`/`resize`/
|
|
9
|
+
* `resize-ack`/`ack`, sources `queek-app`/`queek-merchant`, exact-origin
|
|
10
|
+
* postMessage): v1 only ADDS optional fields and new message types, and both
|
|
11
|
+
* directions ignore unknown types.
|
|
12
|
+
*
|
|
8
13
|
* DOM-free by design (structural minimal types): the app passes its own
|
|
9
14
|
* window/parent in, so this module needs no DOM lib and stays import-safe
|
|
10
15
|
* in edge runtimes.
|
|
11
16
|
*/
|
|
12
17
|
export const DASHBOARD_SOURCE = "queek-merchant";
|
|
13
18
|
export const APP_SOURCE = "queek-app";
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
/** Bridge protocol version this SDK speaks. */
|
|
20
|
+
export const BRIDGE_VERSION = "1";
|
|
21
|
+
/** Client-side length caps: every outbound string is truncated to its cap. */
|
|
22
|
+
export const MAX_HEADING_LENGTH = 200;
|
|
23
|
+
export const MAX_LABEL_LENGTH = 80;
|
|
24
|
+
export const MAX_ACTION_ID_LENGTH = 64;
|
|
25
|
+
export const MAX_TOAST_LENGTH = 500;
|
|
26
|
+
export const MAX_PATH_LENGTH = 2048;
|
|
27
|
+
export const MAX_TARGET_LENGTH = 2048;
|
|
28
|
+
export const MAX_LOCALE_LENGTH = 32;
|
|
29
|
+
export const MAX_SDK_VERSION_LENGTH = 32;
|
|
30
|
+
export const MAX_CAPABILITY_LENGTH = 64;
|
|
31
|
+
export const MAX_CAPABILITIES = 32;
|
|
32
|
+
export const MAX_SECONDARY_ACTIONS = 5;
|
|
33
|
+
export const MAX_SELECTION_IDS = 100;
|
|
34
|
+
export const MAX_SELECTION_ID_LENGTH = 128;
|
|
35
|
+
export const MAX_RESOURCE_TITLE_LENGTH = 300;
|
|
36
|
+
export const MAX_IMAGE_URL_LENGTH = 2048;
|
|
37
|
+
export const MAX_FILTER_LENGTH = 64;
|
|
38
|
+
export const MAX_TOAST_DURATION_MS = 10_000;
|
|
39
|
+
export const MAX_RESIZE_HEIGHT = 10_000;
|
|
40
|
+
/** Dashboard answers carry at most this many picked items; the rest is dropped. */
|
|
41
|
+
export const MAX_PICKED_ITEMS = 100;
|
|
42
|
+
/** Picker request ids are opaque correlation tokens, capped like action ids. */
|
|
43
|
+
export const MAX_REQUEST_ID_LENGTH = 64;
|
|
44
|
+
function isRecord(data) {
|
|
45
|
+
return !!data && typeof data === "object";
|
|
46
|
+
}
|
|
47
|
+
/** Truncate an outbound string client-side. Non-strings become "". */
|
|
48
|
+
export function capText(value, max) {
|
|
49
|
+
const text = typeof value === "string" ? value : "";
|
|
50
|
+
return text.length > max ? text.slice(0, max) : text;
|
|
51
|
+
}
|
|
52
|
+
function optText(value, max) {
|
|
53
|
+
if (value === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return capText(value, max);
|
|
60
|
+
}
|
|
61
|
+
function reqText(value, max) {
|
|
62
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return capText(value, max);
|
|
66
|
+
}
|
|
67
|
+
function isThemeMode(value) {
|
|
68
|
+
return value === "light" || value === "dark";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Control characters never appear in legitimate bridge strings — and
|
|
72
|
+
* browsers strip tab/newline inside schemes (`java\tscript:`), so reject
|
|
73
|
+
* them outright. A char-code loop: regex control ranges trip the linter.
|
|
74
|
+
*/
|
|
75
|
+
function hasControlChars(value) {
|
|
76
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
77
|
+
const code = value.charCodeAt(i);
|
|
78
|
+
if (code <= 0x1f || code === 0x7f) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Validate a title-bar action shape (no caps — `clipOutbound` is the single
|
|
86
|
+
* place that truncates). Returns null for malformed defs.
|
|
87
|
+
*/
|
|
88
|
+
function parseTitleActionDef(value) {
|
|
89
|
+
if (!isRecord(value)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (typeof value.id !== "string" || value.id.length === 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value.label !== "string" || value.label.length === 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (value.tone !== undefined && value.tone !== "default" && value.tone !== "critical") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const def = { id: value.id, label: value.label };
|
|
102
|
+
if (value.tone === "default" || value.tone === "critical") {
|
|
103
|
+
def.tone = value.tone;
|
|
104
|
+
}
|
|
105
|
+
return def;
|
|
106
|
+
}
|
|
107
|
+
/** Opaque picker correlation token: kept capped, dropped when malformed. */
|
|
108
|
+
function optRequestId(value) {
|
|
109
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return capText(value, MAX_REQUEST_ID_LENGTH);
|
|
113
|
+
}
|
|
114
|
+
function parseResourceItem(value) {
|
|
115
|
+
if (!isRecord(value)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const pId = reqText(value.p_id, MAX_SELECTION_ID_LENGTH);
|
|
119
|
+
const title = reqText(value.title, MAX_RESOURCE_TITLE_LENGTH);
|
|
120
|
+
if (pId === null || title === null) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const item = { p_id: pId, title };
|
|
124
|
+
// Optional decoration: kept only when it is an https URL or a
|
|
125
|
+
// dashboard-relative path — never javascript:/data:/etc.
|
|
126
|
+
if (typeof value.image === "string" && isAllowedImageUrl(value.image)) {
|
|
127
|
+
item.image = capText(value.image.trim(), MAX_IMAGE_URL_LENGTH);
|
|
128
|
+
}
|
|
129
|
+
return item;
|
|
130
|
+
}
|
|
131
|
+
/** Image URLs the dashboard may attach to picked items. */
|
|
132
|
+
export function isAllowedImageUrl(image) {
|
|
133
|
+
if (typeof image !== "string" || image.length === 0 || image.length > MAX_IMAGE_URL_LENGTH) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const trimmed = image.trim();
|
|
137
|
+
if (trimmed.length === 0 || hasControlChars(trimmed) || trimmed.includes("\\")) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (trimmed.startsWith("//")) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (trimmed.startsWith("/")) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
return new URL(trimmed).protocol === "https:";
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const ABSOLUTE_SCHEME = /^[a-z][a-z0-9+.-]*:/;
|
|
154
|
+
/**
|
|
155
|
+
* The origin-free half of the open-target policy, shared by `clipOutbound`
|
|
156
|
+
* (which has no origin) and `isAllowedOpenTarget`: no control characters
|
|
157
|
+
* (browsers strip tab/newline inside schemes), no backslashes (browsers read
|
|
158
|
+
* `\` as `/` for special schemes, so `https:\\evil` would escape), no
|
|
159
|
+
* protocol-relative URLs, and any absolute URL must be https — which also
|
|
160
|
+
* refuses javascript:/data:/vbscript:/file:.
|
|
161
|
+
*/
|
|
162
|
+
function isSafeOpenShape(target) {
|
|
163
|
+
if (typeof target !== "string" || target.length === 0 || target.length > MAX_TARGET_LENGTH) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const trimmed = target.trim();
|
|
167
|
+
if (trimmed.length === 0 || hasControlChars(trimmed) || trimmed.includes("\\")) {
|
|
16
168
|
return false;
|
|
17
169
|
}
|
|
18
|
-
|
|
19
|
-
if (message.source !== DASHBOARD_SOURCE || typeof message.type !== "string") {
|
|
170
|
+
if (trimmed.startsWith("//")) {
|
|
20
171
|
return false;
|
|
21
172
|
}
|
|
22
|
-
|
|
23
|
-
|
|
173
|
+
return !ABSOLUTE_SCHEME.test(trimmed.toLowerCase()) || trimmed.toLowerCase().startsWith("https:");
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Open targets the app may ask the dashboard to open: a dashboard-relative
|
|
177
|
+
* reference (resolves under the dashboard origin) or an absolute https URL
|
|
178
|
+
* (the dashboard applies its own allowlist on top). Everything else is
|
|
179
|
+
* refused — see `isSafeOpenShape`. The dashboard re-validates every target.
|
|
180
|
+
*/
|
|
181
|
+
export function isAllowedOpenTarget(target, dashboardOrigin) {
|
|
182
|
+
if (!isSafeOpenShape(target)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const url = new URL(target.trim(), new URL(dashboardOrigin));
|
|
187
|
+
return url.origin === new URL(dashboardOrigin).origin || url.protocol === "https:";
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Parse one dashboard→app message. Returns null for anything else —
|
|
195
|
+
* wrong source, unknown type, malformed shape — so unknown types are
|
|
196
|
+
* silently ignored (forward compatibility, both directions).
|
|
197
|
+
*/
|
|
198
|
+
export function parseInboundMessage(data) {
|
|
199
|
+
if (!isRecord(data)) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (data.source !== DASHBOARD_SOURCE || typeof data.type !== "string") {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
switch (data.type) {
|
|
206
|
+
case "token":
|
|
207
|
+
// Opaque credential: never truncated, never logged.
|
|
208
|
+
return typeof data.token === "string" && data.token.length > 0
|
|
209
|
+
? { source: DASHBOARD_SOURCE, type: "token", token: data.token }
|
|
210
|
+
: null;
|
|
211
|
+
case "resize-ack":
|
|
212
|
+
return { source: DASHBOARD_SOURCE, type: "resize-ack" };
|
|
213
|
+
case "theme": {
|
|
214
|
+
if (!isThemeMode(data.mode)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const message = { source: DASHBOARD_SOURCE, type: "theme", mode: data.mode };
|
|
218
|
+
const locale = optText(data.locale, MAX_LOCALE_LENGTH);
|
|
219
|
+
if (locale !== undefined) {
|
|
220
|
+
message.locale = locale;
|
|
221
|
+
}
|
|
222
|
+
// The dashboard handshake: its own capabilities, same caps as ours.
|
|
223
|
+
if (data.capabilities !== undefined) {
|
|
224
|
+
if (!Array.isArray(data.capabilities)) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
message.capabilities = data.capabilities
|
|
228
|
+
.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
229
|
+
.slice(0, MAX_CAPABILITIES)
|
|
230
|
+
.map((entry) => capText(entry, MAX_CAPABILITY_LENGTH));
|
|
231
|
+
}
|
|
232
|
+
if (data.bridge !== undefined) {
|
|
233
|
+
if (typeof data.bridge !== "string" || data.bridge.length === 0) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
message.bridge = capText(data.bridge, MAX_SDK_VERSION_LENGTH);
|
|
237
|
+
}
|
|
238
|
+
return message;
|
|
239
|
+
}
|
|
240
|
+
case "title-action": {
|
|
241
|
+
const id = reqText(data.id, MAX_ACTION_ID_LENGTH);
|
|
242
|
+
return id === null ? null : { source: DASHBOARD_SOURCE, type: "title-action", id };
|
|
243
|
+
}
|
|
244
|
+
case "save-bar-action":
|
|
245
|
+
return data.action === "save" || data.action === "discard"
|
|
246
|
+
? { source: DASHBOARD_SOURCE, type: "save-bar-action", action: data.action }
|
|
247
|
+
: null;
|
|
248
|
+
case "navigate": {
|
|
249
|
+
const path = reqText(data.path, MAX_PATH_LENGTH);
|
|
250
|
+
return path === null ? null : { source: DASHBOARD_SOURCE, type: "navigate", path };
|
|
251
|
+
}
|
|
252
|
+
case "resource-picked": {
|
|
253
|
+
if (!Array.isArray(data.items)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const items = [];
|
|
257
|
+
for (const raw of data.items.slice(0, MAX_PICKED_ITEMS)) {
|
|
258
|
+
const item = parseResourceItem(raw);
|
|
259
|
+
if (item === null) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
items.push(item);
|
|
263
|
+
}
|
|
264
|
+
const picked = {
|
|
265
|
+
source: DASHBOARD_SOURCE,
|
|
266
|
+
type: "resource-picked",
|
|
267
|
+
items,
|
|
268
|
+
};
|
|
269
|
+
const pickedId = optRequestId(data.requestId);
|
|
270
|
+
if (pickedId !== undefined) {
|
|
271
|
+
picked.requestId = pickedId;
|
|
272
|
+
}
|
|
273
|
+
return picked;
|
|
274
|
+
}
|
|
275
|
+
case "resource-pick-cancelled": {
|
|
276
|
+
const cancelled = {
|
|
277
|
+
source: DASHBOARD_SOURCE,
|
|
278
|
+
type: "resource-pick-cancelled",
|
|
279
|
+
};
|
|
280
|
+
const cancelledId = optRequestId(data.requestId);
|
|
281
|
+
if (cancelledId !== undefined) {
|
|
282
|
+
cancelled.requestId = cancelledId;
|
|
283
|
+
}
|
|
284
|
+
return cancelled;
|
|
285
|
+
}
|
|
286
|
+
default:
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function isToastTone(value) {
|
|
291
|
+
return value === "info" || value === "success" || value === "warning" || value === "critical";
|
|
292
|
+
}
|
|
293
|
+
function clampDurationMs(value) {
|
|
294
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
return Math.min(MAX_TOAST_DURATION_MS, Math.floor(value));
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Parse one app→dashboard message — this parser is what the dashboard runs,
|
|
301
|
+
* so it must be trustworthy. Each branch validates the raw shape strictly
|
|
302
|
+
* (malformed required fields and mistyped optionals reject the message with
|
|
303
|
+
* null; malformed enum values reject too) and then delegates every cap and
|
|
304
|
+
* allow-list to `clipOutbound`, the same sanitizer the senders use — one
|
|
305
|
+
* copy of the policy, both directions. Unknown types return null — ignored,
|
|
306
|
+
* never acted on. Pass the dashboard origin to also gate `open` targets via
|
|
307
|
+
* `isAllowedOpenTarget`.
|
|
308
|
+
*/
|
|
309
|
+
export function parseOutboundMessage(data, dashboardOrigin) {
|
|
310
|
+
if (!isRecord(data)) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if (data.source !== APP_SOURCE || typeof data.type !== "string") {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
switch (data.type) {
|
|
317
|
+
case "ready": {
|
|
318
|
+
if (data.capabilities !== undefined && !Array.isArray(data.capabilities)) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
if (data.sdkVersion !== undefined &&
|
|
322
|
+
(typeof data.sdkVersion !== "string" || data.sdkVersion.length === 0)) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return clipOutbound({
|
|
326
|
+
source: APP_SOURCE,
|
|
327
|
+
type: "ready",
|
|
328
|
+
capabilities: data.capabilities,
|
|
329
|
+
sdkVersion: data.sdkVersion,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
case "resize":
|
|
333
|
+
return typeof data.height === "number"
|
|
334
|
+
? clipOutbound({ source: APP_SOURCE, type: "resize", height: data.height })
|
|
335
|
+
: null;
|
|
336
|
+
case "ack":
|
|
337
|
+
return { source: APP_SOURCE, type: "ack" };
|
|
338
|
+
case "title": {
|
|
339
|
+
if (typeof data.heading !== "string" || data.heading.length === 0) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
if (data.secondaryActions !== undefined && !Array.isArray(data.secondaryActions)) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
const out = { source: APP_SOURCE, type: "title", heading: data.heading };
|
|
346
|
+
if (data.primaryAction !== undefined) {
|
|
347
|
+
const primary = parseTitleActionDef(data.primaryAction);
|
|
348
|
+
if (primary !== null) {
|
|
349
|
+
out.primaryAction = primary;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (data.secondaryActions !== undefined) {
|
|
353
|
+
out.secondaryActions = data.secondaryActions;
|
|
354
|
+
}
|
|
355
|
+
return clipOutbound(out);
|
|
356
|
+
}
|
|
357
|
+
case "toast": {
|
|
358
|
+
if (typeof data.message !== "string" || data.message.length === 0) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
if (data.tone !== undefined && !isToastTone(data.tone)) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
const out = { source: APP_SOURCE, type: "toast", message: data.message };
|
|
365
|
+
if (data.tone !== undefined) {
|
|
366
|
+
out.tone = data.tone;
|
|
367
|
+
}
|
|
368
|
+
if (data.durationMs !== undefined) {
|
|
369
|
+
out.durationMs = data.durationMs;
|
|
370
|
+
}
|
|
371
|
+
return clipOutbound(out);
|
|
372
|
+
}
|
|
373
|
+
case "save-bar":
|
|
374
|
+
return data.state === "dirty" || data.state === "clean"
|
|
375
|
+
? clipOutbound({ source: APP_SOURCE, type: "save-bar", state: data.state })
|
|
376
|
+
: null;
|
|
377
|
+
case "navigated":
|
|
378
|
+
return typeof data.path === "string" && data.path.length > 0
|
|
379
|
+
? clipOutbound({ source: APP_SOURCE, type: "navigated", path: data.path })
|
|
380
|
+
: null;
|
|
381
|
+
case "open": {
|
|
382
|
+
if (typeof data.target !== "string" || data.target.length === 0) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
if (dashboardOrigin !== undefined && !isAllowedOpenTarget(data.target, dashboardOrigin)) {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
return clipOutbound({ source: APP_SOURCE, type: "open", target: data.target });
|
|
389
|
+
}
|
|
390
|
+
case "pick-resource": {
|
|
391
|
+
if (data.resourceType !== "product") {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
if (data.multiple !== undefined && typeof data.multiple !== "boolean") {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
if (data.filter !== undefined && typeof data.filter !== "string") {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
if (data.selectionIds !== undefined && !Array.isArray(data.selectionIds)) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
return clipOutbound({
|
|
404
|
+
source: APP_SOURCE,
|
|
405
|
+
type: "pick-resource",
|
|
406
|
+
resourceType: "product",
|
|
407
|
+
multiple: data.multiple,
|
|
408
|
+
filter: data.filter,
|
|
409
|
+
selectionIds: data.selectionIds,
|
|
410
|
+
requestId: data.requestId,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
default:
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Post one app→dashboard message to the exact origin — never `"*"`.
|
|
419
|
+
* Every message runs through `clipOutbound`; unpostable messages are
|
|
420
|
+
* silently skipped.
|
|
421
|
+
*/
|
|
422
|
+
export function sendBridgeMessage(dashboardOrigin, target, message) {
|
|
423
|
+
const clipped = clipOutbound(message);
|
|
424
|
+
if (clipped !== null) {
|
|
425
|
+
target?.postMessage(clipped, dashboardOrigin);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Build the `ready` announcement (capped). The one shared `ready` builder:
|
|
430
|
+
* `sendReady`, `installAuthFetch`'s refresh, and the React provider all
|
|
431
|
+
* announce through it so the dashboard sees identical capabilities.
|
|
432
|
+
*/
|
|
433
|
+
export function buildReadyMessage(options) {
|
|
434
|
+
return (clipOutbound({
|
|
435
|
+
source: APP_SOURCE,
|
|
436
|
+
type: "ready",
|
|
437
|
+
capabilities: options?.capabilities,
|
|
438
|
+
sdkVersion: options?.sdkVersion,
|
|
439
|
+
}) ?? { source: APP_SOURCE, type: "ready" });
|
|
440
|
+
}
|
|
441
|
+
/** Announce readiness to the exact dashboard origin — never `"*"`. */
|
|
442
|
+
export function sendReady(dashboardOrigin, target, options) {
|
|
443
|
+
sendBridgeMessage(dashboardOrigin, target, buildReadyMessage(options));
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Report the document height for auto-height. Builds raw — `clipOutbound`
|
|
447
|
+
* (via `sendBridgeMessage`) clamps and drops non-finite asks.
|
|
448
|
+
*/
|
|
449
|
+
export function sendResize(dashboardOrigin, target, height) {
|
|
450
|
+
sendBridgeMessage(dashboardOrigin, target, { source: APP_SOURCE, type: "resize", height });
|
|
451
|
+
}
|
|
452
|
+
/** Delivery confirmation with no state — accepted and ignored by decision. */
|
|
453
|
+
export function sendAck(dashboardOrigin, target) {
|
|
454
|
+
sendBridgeMessage(dashboardOrigin, target, { source: APP_SOURCE, type: "ack" });
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Drive the dashboard title bar (heading + up to 5 secondary actions).
|
|
458
|
+
* Builds raw — malformed defs are dropped, caps applied downstream.
|
|
459
|
+
*/
|
|
460
|
+
export function sendTitle(dashboardOrigin, target, heading, actions) {
|
|
461
|
+
const message = { source: APP_SOURCE, type: "title", heading };
|
|
462
|
+
const primary = actions?.primaryAction !== undefined ? parseTitleActionDef(actions.primaryAction) : null;
|
|
463
|
+
if (primary !== null) {
|
|
464
|
+
message.primaryAction = primary;
|
|
465
|
+
}
|
|
466
|
+
if (actions?.secondaryActions !== undefined) {
|
|
467
|
+
message.secondaryActions = actions.secondaryActions;
|
|
24
468
|
}
|
|
25
|
-
|
|
469
|
+
sendBridgeMessage(dashboardOrigin, target, message);
|
|
470
|
+
}
|
|
471
|
+
/** Ask the dashboard to show a toast. Builds raw — tone/duration sanitized downstream. */
|
|
472
|
+
export function sendToast(dashboardOrigin, target, message, options) {
|
|
473
|
+
const out = { source: APP_SOURCE, type: "toast", message };
|
|
474
|
+
if (options?.tone !== undefined) {
|
|
475
|
+
out.tone = options.tone;
|
|
476
|
+
}
|
|
477
|
+
if (options?.durationMs !== undefined) {
|
|
478
|
+
out.durationMs = options.durationMs;
|
|
479
|
+
}
|
|
480
|
+
sendBridgeMessage(dashboardOrigin, target, out);
|
|
481
|
+
}
|
|
482
|
+
/** Show (dirty) or hide (clean) the dashboard save bar. Bad states are dropped downstream. */
|
|
483
|
+
export function sendSaveBar(dashboardOrigin, target, state) {
|
|
484
|
+
sendBridgeMessage(dashboardOrigin, target, { source: APP_SOURCE, type: "save-bar", state });
|
|
485
|
+
}
|
|
486
|
+
/** Report an in-app move so the dashboard can mirror it in its URL (U2b). */
|
|
487
|
+
export function sendNavigated(dashboardOrigin, target, path) {
|
|
488
|
+
sendBridgeMessage(dashboardOrigin, target, { source: APP_SOURCE, type: "navigated", path });
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Ask the dashboard to open a dashboard path or https URL (sandbox-safe).
|
|
492
|
+
* Anything `isAllowedOpenTarget` refuses is never posted — the dashboard
|
|
493
|
+
* re-validates every target before acting on it.
|
|
494
|
+
*/
|
|
495
|
+
export function sendOpen(dashboardOrigin, target, openTarget) {
|
|
496
|
+
if (!isAllowedOpenTarget(openTarget, dashboardOrigin)) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
sendBridgeMessage(dashboardOrigin, target, { source: APP_SOURCE, type: "open", target: openTarget });
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Ask the dashboard to render its product picker (one at a time).
|
|
503
|
+
* Builds raw — `clipOutbound` validates, caps, and drops non-product asks.
|
|
504
|
+
*/
|
|
505
|
+
export function sendPickResource(dashboardOrigin, target, request) {
|
|
506
|
+
if (!isRecord(request)) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
sendBridgeMessage(dashboardOrigin, target, {
|
|
510
|
+
source: APP_SOURCE,
|
|
511
|
+
type: "pick-resource",
|
|
512
|
+
resourceType: request.resourceType,
|
|
513
|
+
multiple: request.multiple,
|
|
514
|
+
filter: request.filter,
|
|
515
|
+
selectionIds: request.selectionIds,
|
|
516
|
+
requestId: request.requestId,
|
|
517
|
+
});
|
|
26
518
|
}
|
|
27
519
|
/**
|
|
28
520
|
* Listen for dashboard messages, accepting only the exact dashboard
|
|
29
|
-
* origin.
|
|
521
|
+
* origin. Unknown types are ignored. Returns an unsubscribe function.
|
|
522
|
+
* Never touches any secret.
|
|
30
523
|
*/
|
|
31
524
|
export function listenToDashboard(options) {
|
|
32
|
-
const { dashboardOrigin, onToken, onResizeAck } = options;
|
|
525
|
+
const { dashboardOrigin, onToken, onResizeAck, onTheme, onTitleAction, onSaveBarAction, onNavigate, onResourcePicked, onResourcePickCancelled, } = options;
|
|
33
526
|
const target = options.target;
|
|
527
|
+
const expectSource = options.expectSource;
|
|
34
528
|
const onMessage = (event) => {
|
|
35
529
|
if (event.origin !== dashboardOrigin) {
|
|
36
530
|
return;
|
|
37
531
|
}
|
|
38
|
-
if (
|
|
532
|
+
if (expectSource !== undefined && event.source !== expectSource) {
|
|
39
533
|
return;
|
|
40
534
|
}
|
|
41
|
-
|
|
42
|
-
|
|
535
|
+
const message = parseInboundMessage(event.data);
|
|
536
|
+
if (message === null) {
|
|
537
|
+
return;
|
|
43
538
|
}
|
|
44
|
-
|
|
45
|
-
|
|
539
|
+
switch (message.type) {
|
|
540
|
+
case "token":
|
|
541
|
+
onToken?.(message.token);
|
|
542
|
+
break;
|
|
543
|
+
case "resize-ack":
|
|
544
|
+
onResizeAck?.();
|
|
545
|
+
break;
|
|
546
|
+
case "theme": {
|
|
547
|
+
const theme = { mode: message.mode };
|
|
548
|
+
if (message.locale !== undefined) {
|
|
549
|
+
theme.locale = message.locale;
|
|
550
|
+
}
|
|
551
|
+
if (message.capabilities !== undefined) {
|
|
552
|
+
theme.capabilities = message.capabilities;
|
|
553
|
+
}
|
|
554
|
+
if (message.bridge !== undefined) {
|
|
555
|
+
theme.bridge = message.bridge;
|
|
556
|
+
}
|
|
557
|
+
onTheme?.(theme);
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
case "title-action":
|
|
561
|
+
onTitleAction?.(message.id);
|
|
562
|
+
break;
|
|
563
|
+
case "save-bar-action":
|
|
564
|
+
onSaveBarAction?.(message.action);
|
|
565
|
+
break;
|
|
566
|
+
case "navigate":
|
|
567
|
+
onNavigate?.(message.path);
|
|
568
|
+
break;
|
|
569
|
+
case "resource-picked":
|
|
570
|
+
onResourcePicked?.(message.items, message.requestId);
|
|
571
|
+
break;
|
|
572
|
+
case "resource-pick-cancelled":
|
|
573
|
+
onResourcePickCancelled?.(message.requestId);
|
|
574
|
+
break;
|
|
46
575
|
}
|
|
47
576
|
};
|
|
48
577
|
target?.addEventListener("message", onMessage);
|
|
49
578
|
return () => target?.removeEventListener("message", onMessage);
|
|
50
579
|
}
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
580
|
+
/** Validate a title-bar action shape, then cap its strings. */
|
|
581
|
+
function clipTitleActionDef(value) {
|
|
582
|
+
const def = parseTitleActionDef(value);
|
|
583
|
+
if (def === null) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
const out = {
|
|
587
|
+
id: capText(def.id, MAX_ACTION_ID_LENGTH),
|
|
588
|
+
label: capText(def.label, MAX_LABEL_LENGTH),
|
|
589
|
+
};
|
|
590
|
+
if (def.tone !== undefined) {
|
|
591
|
+
out.tone = def.tone;
|
|
592
|
+
}
|
|
593
|
+
return out;
|
|
594
|
+
}
|
|
595
|
+
function clipSecondaryActions(value) {
|
|
596
|
+
if (!Array.isArray(value)) {
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
const rest = [];
|
|
600
|
+
for (const raw of value.slice(0, MAX_SECONDARY_ACTIONS)) {
|
|
601
|
+
const def = clipTitleActionDef(raw);
|
|
602
|
+
if (def !== null) {
|
|
603
|
+
rest.push(def);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return rest.length > 0 ? rest : undefined;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* The single choke point for everything the app posts: caps every string,
|
|
610
|
+
* allow-lists every enum, clamps every number, re-gates `open` targets
|
|
611
|
+
* against scheme attacks. Returns null when the message is unpostable
|
|
612
|
+
* (malformed required field, bad save-bar state, non-finite height,
|
|
613
|
+
* blocked open target, non-product pick) — `sendBridgeMessage` skips null.
|
|
614
|
+
*/
|
|
615
|
+
export function clipOutbound(message) {
|
|
616
|
+
switch (message.type) {
|
|
617
|
+
case "ready": {
|
|
618
|
+
const out = { source: APP_SOURCE, type: "ready" };
|
|
619
|
+
if (Array.isArray(message.capabilities)) {
|
|
620
|
+
out.capabilities = message.capabilities
|
|
621
|
+
.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
622
|
+
.slice(0, MAX_CAPABILITIES)
|
|
623
|
+
.map((entry) => capText(entry, MAX_CAPABILITY_LENGTH));
|
|
624
|
+
}
|
|
625
|
+
const sdkVersion = optText(message.sdkVersion, MAX_SDK_VERSION_LENGTH);
|
|
626
|
+
if (sdkVersion !== undefined) {
|
|
627
|
+
out.sdkVersion = sdkVersion;
|
|
628
|
+
}
|
|
629
|
+
return out;
|
|
630
|
+
}
|
|
631
|
+
case "resize":
|
|
632
|
+
if (typeof message.height !== "number" || !Number.isFinite(message.height)) {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
source: APP_SOURCE,
|
|
637
|
+
type: "resize",
|
|
638
|
+
height: Math.min(MAX_RESIZE_HEIGHT, Math.max(0, Math.round(message.height))),
|
|
639
|
+
};
|
|
640
|
+
case "ack":
|
|
641
|
+
return { source: APP_SOURCE, type: "ack" };
|
|
642
|
+
case "title": {
|
|
643
|
+
if (typeof message.heading !== "string" || message.heading.length === 0) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
const out = {
|
|
647
|
+
source: APP_SOURCE,
|
|
648
|
+
type: "title",
|
|
649
|
+
heading: capText(message.heading, MAX_HEADING_LENGTH),
|
|
650
|
+
};
|
|
651
|
+
if (message.primaryAction !== undefined) {
|
|
652
|
+
const primary = clipTitleActionDef(message.primaryAction);
|
|
653
|
+
if (primary !== null) {
|
|
654
|
+
out.primaryAction = primary;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const rest = clipSecondaryActions(message.secondaryActions);
|
|
658
|
+
if (rest !== undefined) {
|
|
659
|
+
out.secondaryActions = rest;
|
|
660
|
+
}
|
|
661
|
+
return out;
|
|
662
|
+
}
|
|
663
|
+
case "toast": {
|
|
664
|
+
if (typeof message.message !== "string" || message.message.length === 0) {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
const out = {
|
|
668
|
+
source: APP_SOURCE,
|
|
669
|
+
type: "toast",
|
|
670
|
+
message: capText(message.message, MAX_TOAST_LENGTH),
|
|
671
|
+
};
|
|
672
|
+
if (message.tone !== undefined && isToastTone(message.tone)) {
|
|
673
|
+
out.tone = message.tone;
|
|
674
|
+
}
|
|
675
|
+
const durationMs = clampDurationMs(message.durationMs);
|
|
676
|
+
if (durationMs !== undefined) {
|
|
677
|
+
out.durationMs = durationMs;
|
|
678
|
+
}
|
|
679
|
+
return out;
|
|
680
|
+
}
|
|
681
|
+
case "save-bar":
|
|
682
|
+
return message.state === "dirty" || message.state === "clean"
|
|
683
|
+
? { source: APP_SOURCE, type: "save-bar", state: message.state }
|
|
684
|
+
: null;
|
|
685
|
+
case "navigated":
|
|
686
|
+
return typeof message.path === "string" && message.path.length > 0
|
|
687
|
+
? { source: APP_SOURCE, type: "navigated", path: capText(message.path, MAX_PATH_LENGTH) }
|
|
688
|
+
: null;
|
|
689
|
+
case "open": {
|
|
690
|
+
if (typeof message.target !== "string" || message.target.length === 0) {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
if (!isSafeOpenShape(message.target)) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
return { source: APP_SOURCE, type: "open", target: capText(message.target, MAX_TARGET_LENGTH) };
|
|
697
|
+
}
|
|
698
|
+
case "pick-resource": {
|
|
699
|
+
if (message.resourceType !== "product") {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
const out = {
|
|
703
|
+
source: APP_SOURCE,
|
|
704
|
+
type: "pick-resource",
|
|
705
|
+
resourceType: "product",
|
|
706
|
+
};
|
|
707
|
+
if (message.multiple !== undefined && typeof message.multiple === "boolean") {
|
|
708
|
+
out.multiple = message.multiple;
|
|
709
|
+
}
|
|
710
|
+
const filter = optText(message.filter, MAX_FILTER_LENGTH);
|
|
711
|
+
if (filter !== undefined) {
|
|
712
|
+
out.filter = filter;
|
|
713
|
+
}
|
|
714
|
+
if (message.selectionIds !== undefined && Array.isArray(message.selectionIds)) {
|
|
715
|
+
const ids = message.selectionIds
|
|
716
|
+
.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
717
|
+
.slice(0, MAX_SELECTION_IDS)
|
|
718
|
+
.map((entry) => capText(entry, MAX_SELECTION_ID_LENGTH));
|
|
719
|
+
if (ids.length > 0) {
|
|
720
|
+
out.selectionIds = ids;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const requestId = optRequestId(message.requestId);
|
|
724
|
+
if (requestId !== undefined) {
|
|
725
|
+
out.requestId = requestId;
|
|
726
|
+
}
|
|
727
|
+
return out;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
55
730
|
}
|
|
56
731
|
//# sourceMappingURL=frame.js.map
|