@frontmcp/ui 1.2.1 → 1.4.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/auth/AuthPageWrapper.d.ts +56 -0
- package/auth/AuthPageWrapper.d.ts.map +1 -0
- package/auth/context.d.ts +23 -0
- package/auth/context.d.ts.map +1 -0
- package/auth/contract.d.ts +276 -0
- package/auth/contract.d.ts.map +1 -0
- package/auth/hooks.d.ts +83 -0
- package/auth/hooks.d.ts.map +1 -0
- package/auth/hydrate.d.ts +50 -0
- package/auth/hydrate.d.ts.map +1 -0
- package/auth/index.d.ts +23 -0
- package/auth/index.d.ts.map +1 -0
- package/auth/index.js +421 -0
- package/auth/vanilla/auth-flow.d.ts +96 -0
- package/auth/vanilla/auth-flow.d.ts.map +1 -0
- package/auth/vanilla/index.d.ts +14 -0
- package/auth/vanilla/index.d.ts.map +1 -0
- package/auth/vanilla/index.js +251 -0
- package/bridge/adapters/base-adapter.d.ts +2 -1
- package/bridge/adapters/base-adapter.d.ts.map +1 -1
- package/bridge/adapters/ext-apps.adapter.d.ts +6 -1
- package/bridge/adapters/ext-apps.adapter.d.ts.map +1 -1
- package/bridge/adapters/openai.adapter.d.ts +9 -1
- package/bridge/adapters/openai.adapter.d.ts.map +1 -1
- package/bridge/core/bridge-factory.d.ts +7 -2
- package/bridge/core/bridge-factory.d.ts.map +1 -1
- package/bridge/index.d.ts +1 -1
- package/bridge/index.d.ts.map +1 -1
- package/bridge/index.js +38 -0
- package/bridge/runtime/iife-generator.d.ts.map +1 -1
- package/bridge/types.d.ts +24 -0
- package/bridge/types.d.ts.map +1 -1
- package/components/Modal/Modal.d.ts +1 -1
- package/esm/auth/index.mjs +398 -0
- package/esm/auth/vanilla/index.mjs +228 -0
- package/esm/bridge/index.mjs +38 -0
- package/esm/index.mjs +8 -0
- package/esm/package.json +22 -2
- package/esm/react/index.mjs +8 -0
- package/index.js +8 -0
- package/package.json +22 -2
- package/react/index.js +8 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// libs/ui/src/auth/contract.ts
|
|
2
|
+
var AUTH_FLOW_GLOBAL_KEY = "__FRONTMCP_AUTH__";
|
|
3
|
+
var DEFAULT_SUBMIT_METHOD = "GET";
|
|
4
|
+
var AUTH_WIRE_FIELDS = {
|
|
5
|
+
/** Pending authorization id. */
|
|
6
|
+
pendingAuthId: "pending_auth_id",
|
|
7
|
+
/** Anti-CSRF token. */
|
|
8
|
+
csrf: "csrf",
|
|
9
|
+
/** Marks a consent-form submission (distinguishes empty-select from first visit). */
|
|
10
|
+
consentSubmitted: "consent_submitted",
|
|
11
|
+
/** Repeated checkbox field carrying selected tool ids on the consent slot. */
|
|
12
|
+
tools: "tools",
|
|
13
|
+
/** Marks a federated submission. */
|
|
14
|
+
federated: "federated",
|
|
15
|
+
/** Repeated checkbox field carrying selected provider ids on the federated slot. */
|
|
16
|
+
providers: "providers",
|
|
17
|
+
/** Marks an incremental authorization. */
|
|
18
|
+
incremental: "incremental",
|
|
19
|
+
/** Target app id for incremental authorization. */
|
|
20
|
+
appId: "app_id",
|
|
21
|
+
/** Generic action discriminator (e.g. consent `authorize`/`skip`, or an extra name). */
|
|
22
|
+
action: "action"
|
|
23
|
+
};
|
|
24
|
+
var CONSENT_SUBMITTED_VALUE = "1";
|
|
25
|
+
var WIRE_TRUE = "true";
|
|
26
|
+
var AUTH_EXTRA_FIELD = AUTH_WIRE_FIELDS.action;
|
|
27
|
+
var DEFAULT_AUTH_MOUNT_ID = "frontmcp-auth-root";
|
|
28
|
+
|
|
29
|
+
// libs/ui/src/auth/vanilla/auth-flow.ts
|
|
30
|
+
function getGlobalCarrier() {
|
|
31
|
+
if (typeof window !== "undefined") {
|
|
32
|
+
return window;
|
|
33
|
+
}
|
|
34
|
+
return globalThis;
|
|
35
|
+
}
|
|
36
|
+
function getAuthFlow() {
|
|
37
|
+
const state = tryGetAuthFlow();
|
|
38
|
+
if (!state) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`[auth-ui] No injected auth flow state found on window.${AUTH_FLOW_GLOBAL_KEY}. This page must be server-rendered by a FrontMCP @AuthUi slot.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
function tryGetAuthFlow() {
|
|
46
|
+
const carrier = getGlobalCarrier();
|
|
47
|
+
const state = carrier?.[AUTH_FLOW_GLOBAL_KEY];
|
|
48
|
+
if (!state || typeof state !== "object") {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
return state;
|
|
52
|
+
}
|
|
53
|
+
function getAddedItems(name) {
|
|
54
|
+
const items = tryGetAuthFlow()?.addedItems?.[name];
|
|
55
|
+
return Array.isArray(items) ? items : [];
|
|
56
|
+
}
|
|
57
|
+
function toEntries(input) {
|
|
58
|
+
if (!input) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
if (isFormElement(input)) {
|
|
62
|
+
return formDataToEntries(new FormData(input));
|
|
63
|
+
}
|
|
64
|
+
if (typeof FormData !== "undefined" && input instanceof FormData) {
|
|
65
|
+
return formDataToEntries(input);
|
|
66
|
+
}
|
|
67
|
+
const entries = [];
|
|
68
|
+
for (const [key, value] of Object.entries(input)) {
|
|
69
|
+
if (value === void 0 || value === null) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
for (const v of value) {
|
|
74
|
+
if (v !== void 0 && v !== null) {
|
|
75
|
+
entries.push([key, String(v)]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
entries.push([key, String(value)]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return entries;
|
|
83
|
+
}
|
|
84
|
+
function formDataToEntries(fd) {
|
|
85
|
+
const entries = [];
|
|
86
|
+
fd.forEach((value, key) => {
|
|
87
|
+
if (typeof value === "string") {
|
|
88
|
+
entries.push([key, value]);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return entries;
|
|
92
|
+
}
|
|
93
|
+
function isFormElement(input) {
|
|
94
|
+
return typeof input === "object" && // `nodeName` + a `FormData`-constructible shape is enough; avoids requiring
|
|
95
|
+
// a live `HTMLFormElement` global (jsdom provides one, node does not).
|
|
96
|
+
input.nodeName === "FORM";
|
|
97
|
+
}
|
|
98
|
+
function buildGetUrl(base, entries) {
|
|
99
|
+
const origin = typeof window !== "undefined" && window.location ? window.location.origin : void 0;
|
|
100
|
+
const url = origin ? new URL(base, origin) : new URL(base, "http://localhost");
|
|
101
|
+
for (const [key, value] of entries) {
|
|
102
|
+
url.searchParams.append(key, value);
|
|
103
|
+
}
|
|
104
|
+
if (!origin && !/^https?:\/\//i.test(base)) {
|
|
105
|
+
return `${url.pathname}${url.search}`;
|
|
106
|
+
}
|
|
107
|
+
return url.toString();
|
|
108
|
+
}
|
|
109
|
+
function buildUrlEncodedBody(entries) {
|
|
110
|
+
const params = new URLSearchParams();
|
|
111
|
+
for (const [key, value] of entries) {
|
|
112
|
+
params.append(key, value);
|
|
113
|
+
}
|
|
114
|
+
return params.toString();
|
|
115
|
+
}
|
|
116
|
+
function withControlFields(state, entries, markers) {
|
|
117
|
+
const present = new Set(entries.map(([k]) => k));
|
|
118
|
+
const merged = [...entries];
|
|
119
|
+
const ensure = (key, value) => {
|
|
120
|
+
if (value !== void 0 && !present.has(key)) {
|
|
121
|
+
merged.push([key, value]);
|
|
122
|
+
present.add(key);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
ensure(AUTH_WIRE_FIELDS.pendingAuthId, state.pendingAuthId);
|
|
126
|
+
ensure(AUTH_WIRE_FIELDS.csrf, state.csrfToken);
|
|
127
|
+
for (const [key, value] of markers) {
|
|
128
|
+
ensure(key, value);
|
|
129
|
+
}
|
|
130
|
+
return merged;
|
|
131
|
+
}
|
|
132
|
+
function finishMarkers(state) {
|
|
133
|
+
switch (state.slot) {
|
|
134
|
+
case "consent":
|
|
135
|
+
return [[AUTH_WIRE_FIELDS.consentSubmitted, CONSENT_SUBMITTED_VALUE]];
|
|
136
|
+
default:
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function dispatch(method, url, entries) {
|
|
141
|
+
if (typeof fetch !== "function") {
|
|
142
|
+
throw new Error("[auth-ui] global fetch is unavailable in this environment");
|
|
143
|
+
}
|
|
144
|
+
if (method === "GET") {
|
|
145
|
+
return fetch(buildGetUrl(url, entries), {
|
|
146
|
+
method: "GET",
|
|
147
|
+
credentials: "same-origin",
|
|
148
|
+
headers: { Accept: "application/json, text/html" }
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return fetch(url, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
credentials: "same-origin",
|
|
154
|
+
headers: {
|
|
155
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
156
|
+
Accept: "application/json, text/html"
|
|
157
|
+
},
|
|
158
|
+
body: buildUrlEncodedBody(entries)
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
var defaultNavigator = (url) => {
|
|
162
|
+
if (typeof window !== "undefined" && window.location) {
|
|
163
|
+
window.location.assign(url);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
var currentNavigator = defaultNavigator;
|
|
167
|
+
function setAuthNavigator(navigator) {
|
|
168
|
+
currentNavigator = navigator ?? defaultNavigator;
|
|
169
|
+
}
|
|
170
|
+
async function submitFinish(formOrData, options = {}) {
|
|
171
|
+
const state = options.state ?? getAuthFlow();
|
|
172
|
+
if (!state.submitUrl) {
|
|
173
|
+
throw new Error("[auth-ui] submitFinish called but the injected flow state has no submitUrl");
|
|
174
|
+
}
|
|
175
|
+
const method = state.submitMethod ?? DEFAULT_SUBMIT_METHOD;
|
|
176
|
+
const entries = withControlFields(state, toEntries(formOrData), finishMarkers(state));
|
|
177
|
+
const response = await dispatch(method, state.submitUrl, entries);
|
|
178
|
+
const shouldNavigate = options.navigate ?? (typeof window !== "undefined" && !!window.location);
|
|
179
|
+
if (shouldNavigate && response.redirected && response.url) {
|
|
180
|
+
currentNavigator(response.url);
|
|
181
|
+
}
|
|
182
|
+
return response;
|
|
183
|
+
}
|
|
184
|
+
async function submitExtra(name, data, state) {
|
|
185
|
+
const flow = state ?? getAuthFlow();
|
|
186
|
+
const target = flow.extraUrl ?? flow.submitUrl;
|
|
187
|
+
if (!target) {
|
|
188
|
+
throw new Error("[auth-ui] submitExtra called but the injected flow state has no extraUrl/submitUrl");
|
|
189
|
+
}
|
|
190
|
+
const markers = flow.extraUrl ? [] : [[AUTH_EXTRA_FIELD, name]];
|
|
191
|
+
const entries = withControlFields(flow, toEntries(data), markers);
|
|
192
|
+
const response = await dispatch("POST", target, entries);
|
|
193
|
+
return parseExtraResponse(response);
|
|
194
|
+
}
|
|
195
|
+
async function parseExtraResponse(response) {
|
|
196
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
197
|
+
if (contentType.includes("application/json")) {
|
|
198
|
+
try {
|
|
199
|
+
const body = await response.json();
|
|
200
|
+
return {
|
|
201
|
+
ok: body.ok ?? response.ok,
|
|
202
|
+
error: typeof body.error === "string" ? body.error : void 0,
|
|
203
|
+
addedItems: body.addedItems && typeof body.addedItems === "object" ? body.addedItems : void 0,
|
|
204
|
+
sideEffects: body.sideEffects && typeof body.sideEffects === "object" ? body.sideEffects : void 0
|
|
205
|
+
};
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (response.ok) {
|
|
210
|
+
return { ok: true };
|
|
211
|
+
}
|
|
212
|
+
return { ok: false, error: `Request failed with status ${response.status}` };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// libs/ui/src/auth/context.tsx
|
|
216
|
+
import { createContext, useContext } from "react";
|
|
217
|
+
var AuthFlowContext = createContext(null);
|
|
218
|
+
function useAuthFlowContext() {
|
|
219
|
+
const ctx = useContext(AuthFlowContext);
|
|
220
|
+
if (!ctx) {
|
|
221
|
+
throw new Error("[auth-ui] useAuthFlow* hooks must be used inside <AuthPageWrapper> / <AuthFlowProvider>");
|
|
222
|
+
}
|
|
223
|
+
return ctx;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// libs/ui/src/auth/AuthPageWrapper.tsx
|
|
227
|
+
import { useContext as useContext2, useMemo, useState } from "react";
|
|
228
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
229
|
+
function AuthFlowProvider({
|
|
230
|
+
children,
|
|
231
|
+
state: stateOverride
|
|
232
|
+
}) {
|
|
233
|
+
const injected = stateOverride ?? tryGetAuthFlow();
|
|
234
|
+
const [state, setState] = useState(
|
|
235
|
+
injected ?? { slot: "error", error: "No authorization flow state was provided." }
|
|
236
|
+
);
|
|
237
|
+
const value = useMemo(
|
|
238
|
+
() => ({
|
|
239
|
+
state,
|
|
240
|
+
update: (patch) => setState((prev) => ({ ...prev, ...patch }))
|
|
241
|
+
}),
|
|
242
|
+
[state]
|
|
243
|
+
);
|
|
244
|
+
return /* @__PURE__ */ jsx(AuthFlowContext.Provider, { value, children });
|
|
245
|
+
}
|
|
246
|
+
function ControlFields({ state }) {
|
|
247
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
248
|
+
state.pendingAuthId !== void 0 && /* @__PURE__ */ jsx("input", { type: "hidden", name: AUTH_WIRE_FIELDS.pendingAuthId, value: state.pendingAuthId }),
|
|
249
|
+
state.csrfToken !== void 0 && /* @__PURE__ */ jsx("input", { type: "hidden", name: AUTH_WIRE_FIELDS.csrf, value: state.csrfToken }),
|
|
250
|
+
state.slot === "consent" && /* @__PURE__ */ jsx("input", { type: "hidden", name: AUTH_WIRE_FIELDS.consentSubmitted, value: CONSENT_SUBMITTED_VALUE })
|
|
251
|
+
] });
|
|
252
|
+
}
|
|
253
|
+
function AuthPageWrapper({
|
|
254
|
+
children,
|
|
255
|
+
state: stateOverride,
|
|
256
|
+
renderForm = true,
|
|
257
|
+
className
|
|
258
|
+
}) {
|
|
259
|
+
return /* @__PURE__ */ jsx(AuthFlowProvider, { state: stateOverride, children: /* @__PURE__ */ jsx(AuthPageWrapperInner, { renderForm, className, children }) });
|
|
260
|
+
}
|
|
261
|
+
function AuthPageWrapperInner({
|
|
262
|
+
children,
|
|
263
|
+
renderForm,
|
|
264
|
+
className
|
|
265
|
+
}) {
|
|
266
|
+
const ctx = useAuthFlowContextSafe();
|
|
267
|
+
const state = ctx?.state;
|
|
268
|
+
if (!renderForm || !state?.submitUrl) {
|
|
269
|
+
return /* @__PURE__ */ jsx("div", { className: className ?? "frontmcp-auth-page", children });
|
|
270
|
+
}
|
|
271
|
+
const method = (state.submitMethod ?? DEFAULT_SUBMIT_METHOD).toLowerCase();
|
|
272
|
+
return /* @__PURE__ */ jsx("div", { className: className ?? "frontmcp-auth-page", children: /* @__PURE__ */ jsxs("form", { action: state.submitUrl, method, children: [
|
|
273
|
+
/* @__PURE__ */ jsx(ControlFields, { state }),
|
|
274
|
+
children
|
|
275
|
+
] }) });
|
|
276
|
+
}
|
|
277
|
+
function useAuthFlowContextSafe() {
|
|
278
|
+
return useContext2(AuthFlowContext);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// libs/ui/src/auth/hooks.ts
|
|
282
|
+
import { useCallback, useState as useState2 } from "react";
|
|
283
|
+
function normalizeFormArg(arg) {
|
|
284
|
+
if (!arg) {
|
|
285
|
+
return void 0;
|
|
286
|
+
}
|
|
287
|
+
const maybeEvent = arg;
|
|
288
|
+
if (typeof maybeEvent.preventDefault === "function" && maybeEvent.currentTarget) {
|
|
289
|
+
maybeEvent.preventDefault();
|
|
290
|
+
return maybeEvent.currentTarget;
|
|
291
|
+
}
|
|
292
|
+
return arg;
|
|
293
|
+
}
|
|
294
|
+
function useAuthFlow() {
|
|
295
|
+
const { state } = useAuthFlowContext();
|
|
296
|
+
const submitFinish2 = useCallback(
|
|
297
|
+
(formOrEvent) => submitFinish(normalizeFormArg(formOrEvent), { state }),
|
|
298
|
+
[state]
|
|
299
|
+
);
|
|
300
|
+
return {
|
|
301
|
+
slot: state.slot,
|
|
302
|
+
pendingAuthId: state.pendingAuthId,
|
|
303
|
+
clientName: state.clientName,
|
|
304
|
+
clientId: state.clientId,
|
|
305
|
+
scopes: state.scopes ?? [],
|
|
306
|
+
redirectUri: state.redirectUri,
|
|
307
|
+
resource: state.resource,
|
|
308
|
+
error: state.error,
|
|
309
|
+
providers: state.providers ?? [],
|
|
310
|
+
tools: state.tools ?? [],
|
|
311
|
+
extras: state.extras ?? {},
|
|
312
|
+
state,
|
|
313
|
+
submitFinish: submitFinish2
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function useAddedItems(name) {
|
|
317
|
+
const { state } = useAuthFlowContext();
|
|
318
|
+
const items = state.addedItems?.[name];
|
|
319
|
+
return Array.isArray(items) ? items : [];
|
|
320
|
+
}
|
|
321
|
+
function useExtraField(name) {
|
|
322
|
+
const { state, update } = useAuthFlowContext();
|
|
323
|
+
const [result, setResult] = useState2(void 0);
|
|
324
|
+
const [pending, setPending] = useState2(false);
|
|
325
|
+
const onSubmit = useCallback(
|
|
326
|
+
async (formOrEvent) => {
|
|
327
|
+
const data = normalizeFormArg(formOrEvent);
|
|
328
|
+
setPending(true);
|
|
329
|
+
try {
|
|
330
|
+
const res = await submitExtra(name, data, state);
|
|
331
|
+
setResult(res);
|
|
332
|
+
if (res.ok && res.addedItems) {
|
|
333
|
+
update({ addedItems: res.addedItems });
|
|
334
|
+
}
|
|
335
|
+
return res;
|
|
336
|
+
} catch (err) {
|
|
337
|
+
const failure = {
|
|
338
|
+
ok: false,
|
|
339
|
+
error: err instanceof Error ? err.message : "Failed to submit field"
|
|
340
|
+
};
|
|
341
|
+
setResult(failure);
|
|
342
|
+
return failure;
|
|
343
|
+
} finally {
|
|
344
|
+
setPending(false);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
[name, state, update]
|
|
348
|
+
);
|
|
349
|
+
return { onSubmit, result, pending };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// libs/ui/src/auth/hydrate.tsx
|
|
353
|
+
import { createRoot } from "react-dom/client";
|
|
354
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
355
|
+
function resolveContainer(container) {
|
|
356
|
+
if (container && typeof container !== "string") {
|
|
357
|
+
return container;
|
|
358
|
+
}
|
|
359
|
+
if (typeof document === "undefined") {
|
|
360
|
+
throw new Error("[auth-ui] mountAuthPage requires a DOM (document is undefined)");
|
|
361
|
+
}
|
|
362
|
+
const selector = container ?? `#${DEFAULT_AUTH_MOUNT_ID}`;
|
|
363
|
+
const el = document.querySelector(selector);
|
|
364
|
+
if (!el) {
|
|
365
|
+
throw new Error(`[auth-ui] mountAuthPage could not find a container matching "${selector}"`);
|
|
366
|
+
}
|
|
367
|
+
return el;
|
|
368
|
+
}
|
|
369
|
+
function mountAuthPage(Component, options = {}) {
|
|
370
|
+
const container = resolveContainer(options.container);
|
|
371
|
+
const root = createRoot(container);
|
|
372
|
+
root.render(
|
|
373
|
+
/* @__PURE__ */ jsx2(AuthPageWrapper, { state: options.state, renderForm: options.renderForm, children: /* @__PURE__ */ jsx2(Component, {}) })
|
|
374
|
+
);
|
|
375
|
+
return root;
|
|
376
|
+
}
|
|
377
|
+
export {
|
|
378
|
+
AUTH_EXTRA_FIELD,
|
|
379
|
+
AUTH_FLOW_GLOBAL_KEY,
|
|
380
|
+
AUTH_WIRE_FIELDS,
|
|
381
|
+
AuthFlowContext,
|
|
382
|
+
AuthFlowProvider,
|
|
383
|
+
AuthPageWrapper,
|
|
384
|
+
CONSENT_SUBMITTED_VALUE,
|
|
385
|
+
DEFAULT_AUTH_MOUNT_ID,
|
|
386
|
+
DEFAULT_SUBMIT_METHOD,
|
|
387
|
+
WIRE_TRUE,
|
|
388
|
+
getAddedItems,
|
|
389
|
+
getAuthFlow,
|
|
390
|
+
mountAuthPage,
|
|
391
|
+
setAuthNavigator,
|
|
392
|
+
submitExtra,
|
|
393
|
+
submitFinish,
|
|
394
|
+
tryGetAuthFlow,
|
|
395
|
+
useAddedItems,
|
|
396
|
+
useAuthFlow,
|
|
397
|
+
useExtraField
|
|
398
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// libs/ui/src/auth/contract.ts
|
|
2
|
+
var AUTH_FLOW_GLOBAL_KEY = "__FRONTMCP_AUTH__";
|
|
3
|
+
var DEFAULT_SUBMIT_METHOD = "GET";
|
|
4
|
+
var AUTH_WIRE_FIELDS = {
|
|
5
|
+
/** Pending authorization id. */
|
|
6
|
+
pendingAuthId: "pending_auth_id",
|
|
7
|
+
/** Anti-CSRF token. */
|
|
8
|
+
csrf: "csrf",
|
|
9
|
+
/** Marks a consent-form submission (distinguishes empty-select from first visit). */
|
|
10
|
+
consentSubmitted: "consent_submitted",
|
|
11
|
+
/** Repeated checkbox field carrying selected tool ids on the consent slot. */
|
|
12
|
+
tools: "tools",
|
|
13
|
+
/** Marks a federated submission. */
|
|
14
|
+
federated: "federated",
|
|
15
|
+
/** Repeated checkbox field carrying selected provider ids on the federated slot. */
|
|
16
|
+
providers: "providers",
|
|
17
|
+
/** Marks an incremental authorization. */
|
|
18
|
+
incremental: "incremental",
|
|
19
|
+
/** Target app id for incremental authorization. */
|
|
20
|
+
appId: "app_id",
|
|
21
|
+
/** Generic action discriminator (e.g. consent `authorize`/`skip`, or an extra name). */
|
|
22
|
+
action: "action"
|
|
23
|
+
};
|
|
24
|
+
var CONSENT_SUBMITTED_VALUE = "1";
|
|
25
|
+
var WIRE_TRUE = "true";
|
|
26
|
+
var AUTH_EXTRA_FIELD = AUTH_WIRE_FIELDS.action;
|
|
27
|
+
var DEFAULT_AUTH_MOUNT_ID = "frontmcp-auth-root";
|
|
28
|
+
|
|
29
|
+
// libs/ui/src/auth/vanilla/auth-flow.ts
|
|
30
|
+
function getGlobalCarrier() {
|
|
31
|
+
if (typeof window !== "undefined") {
|
|
32
|
+
return window;
|
|
33
|
+
}
|
|
34
|
+
return globalThis;
|
|
35
|
+
}
|
|
36
|
+
function getAuthFlow() {
|
|
37
|
+
const state = tryGetAuthFlow();
|
|
38
|
+
if (!state) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`[auth-ui] No injected auth flow state found on window.${AUTH_FLOW_GLOBAL_KEY}. This page must be server-rendered by a FrontMCP @AuthUi slot.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
function tryGetAuthFlow() {
|
|
46
|
+
const carrier = getGlobalCarrier();
|
|
47
|
+
const state = carrier?.[AUTH_FLOW_GLOBAL_KEY];
|
|
48
|
+
if (!state || typeof state !== "object") {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
return state;
|
|
52
|
+
}
|
|
53
|
+
function getAddedItems(name) {
|
|
54
|
+
const items = tryGetAuthFlow()?.addedItems?.[name];
|
|
55
|
+
return Array.isArray(items) ? items : [];
|
|
56
|
+
}
|
|
57
|
+
function toEntries(input) {
|
|
58
|
+
if (!input) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
if (isFormElement(input)) {
|
|
62
|
+
return formDataToEntries(new FormData(input));
|
|
63
|
+
}
|
|
64
|
+
if (typeof FormData !== "undefined" && input instanceof FormData) {
|
|
65
|
+
return formDataToEntries(input);
|
|
66
|
+
}
|
|
67
|
+
const entries = [];
|
|
68
|
+
for (const [key, value] of Object.entries(input)) {
|
|
69
|
+
if (value === void 0 || value === null) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
for (const v of value) {
|
|
74
|
+
if (v !== void 0 && v !== null) {
|
|
75
|
+
entries.push([key, String(v)]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
entries.push([key, String(value)]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return entries;
|
|
83
|
+
}
|
|
84
|
+
function formDataToEntries(fd) {
|
|
85
|
+
const entries = [];
|
|
86
|
+
fd.forEach((value, key) => {
|
|
87
|
+
if (typeof value === "string") {
|
|
88
|
+
entries.push([key, value]);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
return entries;
|
|
92
|
+
}
|
|
93
|
+
function isFormElement(input) {
|
|
94
|
+
return typeof input === "object" && // `nodeName` + a `FormData`-constructible shape is enough; avoids requiring
|
|
95
|
+
// a live `HTMLFormElement` global (jsdom provides one, node does not).
|
|
96
|
+
input.nodeName === "FORM";
|
|
97
|
+
}
|
|
98
|
+
function buildGetUrl(base, entries) {
|
|
99
|
+
const origin = typeof window !== "undefined" && window.location ? window.location.origin : void 0;
|
|
100
|
+
const url = origin ? new URL(base, origin) : new URL(base, "http://localhost");
|
|
101
|
+
for (const [key, value] of entries) {
|
|
102
|
+
url.searchParams.append(key, value);
|
|
103
|
+
}
|
|
104
|
+
if (!origin && !/^https?:\/\//i.test(base)) {
|
|
105
|
+
return `${url.pathname}${url.search}`;
|
|
106
|
+
}
|
|
107
|
+
return url.toString();
|
|
108
|
+
}
|
|
109
|
+
function buildUrlEncodedBody(entries) {
|
|
110
|
+
const params = new URLSearchParams();
|
|
111
|
+
for (const [key, value] of entries) {
|
|
112
|
+
params.append(key, value);
|
|
113
|
+
}
|
|
114
|
+
return params.toString();
|
|
115
|
+
}
|
|
116
|
+
function withControlFields(state, entries, markers) {
|
|
117
|
+
const present = new Set(entries.map(([k]) => k));
|
|
118
|
+
const merged = [...entries];
|
|
119
|
+
const ensure = (key, value) => {
|
|
120
|
+
if (value !== void 0 && !present.has(key)) {
|
|
121
|
+
merged.push([key, value]);
|
|
122
|
+
present.add(key);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
ensure(AUTH_WIRE_FIELDS.pendingAuthId, state.pendingAuthId);
|
|
126
|
+
ensure(AUTH_WIRE_FIELDS.csrf, state.csrfToken);
|
|
127
|
+
for (const [key, value] of markers) {
|
|
128
|
+
ensure(key, value);
|
|
129
|
+
}
|
|
130
|
+
return merged;
|
|
131
|
+
}
|
|
132
|
+
function finishMarkers(state) {
|
|
133
|
+
switch (state.slot) {
|
|
134
|
+
case "consent":
|
|
135
|
+
return [[AUTH_WIRE_FIELDS.consentSubmitted, CONSENT_SUBMITTED_VALUE]];
|
|
136
|
+
default:
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function dispatch(method, url, entries) {
|
|
141
|
+
if (typeof fetch !== "function") {
|
|
142
|
+
throw new Error("[auth-ui] global fetch is unavailable in this environment");
|
|
143
|
+
}
|
|
144
|
+
if (method === "GET") {
|
|
145
|
+
return fetch(buildGetUrl(url, entries), {
|
|
146
|
+
method: "GET",
|
|
147
|
+
credentials: "same-origin",
|
|
148
|
+
headers: { Accept: "application/json, text/html" }
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return fetch(url, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
credentials: "same-origin",
|
|
154
|
+
headers: {
|
|
155
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
156
|
+
Accept: "application/json, text/html"
|
|
157
|
+
},
|
|
158
|
+
body: buildUrlEncodedBody(entries)
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
var defaultNavigator = (url) => {
|
|
162
|
+
if (typeof window !== "undefined" && window.location) {
|
|
163
|
+
window.location.assign(url);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
var currentNavigator = defaultNavigator;
|
|
167
|
+
function setAuthNavigator(navigator) {
|
|
168
|
+
currentNavigator = navigator ?? defaultNavigator;
|
|
169
|
+
}
|
|
170
|
+
async function submitFinish(formOrData, options = {}) {
|
|
171
|
+
const state = options.state ?? getAuthFlow();
|
|
172
|
+
if (!state.submitUrl) {
|
|
173
|
+
throw new Error("[auth-ui] submitFinish called but the injected flow state has no submitUrl");
|
|
174
|
+
}
|
|
175
|
+
const method = state.submitMethod ?? DEFAULT_SUBMIT_METHOD;
|
|
176
|
+
const entries = withControlFields(state, toEntries(formOrData), finishMarkers(state));
|
|
177
|
+
const response = await dispatch(method, state.submitUrl, entries);
|
|
178
|
+
const shouldNavigate = options.navigate ?? (typeof window !== "undefined" && !!window.location);
|
|
179
|
+
if (shouldNavigate && response.redirected && response.url) {
|
|
180
|
+
currentNavigator(response.url);
|
|
181
|
+
}
|
|
182
|
+
return response;
|
|
183
|
+
}
|
|
184
|
+
async function submitExtra(name, data, state) {
|
|
185
|
+
const flow = state ?? getAuthFlow();
|
|
186
|
+
const target = flow.extraUrl ?? flow.submitUrl;
|
|
187
|
+
if (!target) {
|
|
188
|
+
throw new Error("[auth-ui] submitExtra called but the injected flow state has no extraUrl/submitUrl");
|
|
189
|
+
}
|
|
190
|
+
const markers = flow.extraUrl ? [] : [[AUTH_EXTRA_FIELD, name]];
|
|
191
|
+
const entries = withControlFields(flow, toEntries(data), markers);
|
|
192
|
+
const response = await dispatch("POST", target, entries);
|
|
193
|
+
return parseExtraResponse(response);
|
|
194
|
+
}
|
|
195
|
+
async function parseExtraResponse(response) {
|
|
196
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
197
|
+
if (contentType.includes("application/json")) {
|
|
198
|
+
try {
|
|
199
|
+
const body = await response.json();
|
|
200
|
+
return {
|
|
201
|
+
ok: body.ok ?? response.ok,
|
|
202
|
+
error: typeof body.error === "string" ? body.error : void 0,
|
|
203
|
+
addedItems: body.addedItems && typeof body.addedItems === "object" ? body.addedItems : void 0,
|
|
204
|
+
sideEffects: body.sideEffects && typeof body.sideEffects === "object" ? body.sideEffects : void 0
|
|
205
|
+
};
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (response.ok) {
|
|
210
|
+
return { ok: true };
|
|
211
|
+
}
|
|
212
|
+
return { ok: false, error: `Request failed with status ${response.status}` };
|
|
213
|
+
}
|
|
214
|
+
export {
|
|
215
|
+
AUTH_EXTRA_FIELD,
|
|
216
|
+
AUTH_FLOW_GLOBAL_KEY,
|
|
217
|
+
AUTH_WIRE_FIELDS,
|
|
218
|
+
CONSENT_SUBMITTED_VALUE,
|
|
219
|
+
DEFAULT_AUTH_MOUNT_ID,
|
|
220
|
+
DEFAULT_SUBMIT_METHOD,
|
|
221
|
+
WIRE_TRUE,
|
|
222
|
+
getAddedItems,
|
|
223
|
+
getAuthFlow,
|
|
224
|
+
setAuthNavigator,
|
|
225
|
+
submitExtra,
|
|
226
|
+
submitFinish,
|
|
227
|
+
tryGetAuthFlow
|
|
228
|
+
};
|
package/esm/bridge/index.mjs
CHANGED
|
@@ -455,6 +455,14 @@ var FrontMcpBridge = class {
|
|
|
455
455
|
const adapter = this._ensureInitialized();
|
|
456
456
|
return adapter.requestDisplayMode(mode);
|
|
457
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Report a desired widget size to the host.
|
|
460
|
+
* @param size - Desired widget dimensions
|
|
461
|
+
*/
|
|
462
|
+
async setSize(size) {
|
|
463
|
+
const adapter = this._ensureInitialized();
|
|
464
|
+
return adapter.setSize(size);
|
|
465
|
+
}
|
|
458
466
|
/**
|
|
459
467
|
* Request widget close.
|
|
460
468
|
*/
|
|
@@ -673,6 +681,8 @@ var BaseAdapter = class {
|
|
|
673
681
|
}
|
|
674
682
|
throw new Error("requestDisplayMode not implemented");
|
|
675
683
|
}
|
|
684
|
+
async setSize(_size) {
|
|
685
|
+
}
|
|
676
686
|
async requestClose() {
|
|
677
687
|
}
|
|
678
688
|
setWidgetState(state) {
|
|
@@ -951,6 +961,23 @@ var OpenAIAdapter = class extends BaseAdapter {
|
|
|
951
961
|
await this._openai.canvas.setDisplayMode(mode);
|
|
952
962
|
this._hostContext = { ...this._hostContext, displayMode: mode };
|
|
953
963
|
}
|
|
964
|
+
/**
|
|
965
|
+
* Report a desired widget size to ChatGPT.
|
|
966
|
+
*
|
|
967
|
+
* The Apps SDK normally measures DOM height itself, so this forwards to the
|
|
968
|
+
* SDK's sizing API only when one is exposed (`setWidgetHeight`) and otherwise
|
|
969
|
+
* no-ops. A `displayMode` hint is forwarded to `setDisplayMode`.
|
|
970
|
+
*/
|
|
971
|
+
async setSize(size) {
|
|
972
|
+
const canvas = this._openai?.canvas;
|
|
973
|
+
if (!canvas) return;
|
|
974
|
+
if (size.displayMode && canvas.setDisplayMode) {
|
|
975
|
+
await canvas.setDisplayMode(size.displayMode);
|
|
976
|
+
}
|
|
977
|
+
if (typeof size.height === "number" && canvas.setWidgetHeight) {
|
|
978
|
+
await canvas.setWidgetHeight(size.height);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
954
981
|
async requestClose() {
|
|
955
982
|
if (this._openai?.canvas?.close) {
|
|
956
983
|
await this._openai.canvas.close();
|
|
@@ -1103,6 +1130,17 @@ var ExtAppsAdapter = class extends BaseAdapter {
|
|
|
1103
1130
|
await this._sendRequest("ui/setDisplayMode", { mode });
|
|
1104
1131
|
this._hostContext = { ...this._hostContext, displayMode: mode };
|
|
1105
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Report a desired widget size to the host via the FrontMCP `ui/setSize`
|
|
1135
|
+
* request (parallels `ui/setDisplayMode`).
|
|
1136
|
+
*/
|
|
1137
|
+
async setSize(size) {
|
|
1138
|
+
await this._sendRequest("ui/setSize", {
|
|
1139
|
+
height: size.height,
|
|
1140
|
+
width: size.width,
|
|
1141
|
+
aspectRatio: size.aspectRatio
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1106
1144
|
async requestClose() {
|
|
1107
1145
|
await this._sendRequest("ui/close", {});
|
|
1108
1146
|
}
|
package/esm/index.mjs
CHANGED
|
@@ -449,6 +449,14 @@ var FrontMcpBridge = class {
|
|
|
449
449
|
const adapter = this._ensureInitialized();
|
|
450
450
|
return adapter.requestDisplayMode(mode);
|
|
451
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Report a desired widget size to the host.
|
|
454
|
+
* @param size - Desired widget dimensions
|
|
455
|
+
*/
|
|
456
|
+
async setSize(size) {
|
|
457
|
+
const adapter = this._ensureInitialized();
|
|
458
|
+
return adapter.setSize(size);
|
|
459
|
+
}
|
|
452
460
|
/**
|
|
453
461
|
* Request widget close.
|
|
454
462
|
*/
|