@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
package/auth/index.js
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// libs/ui/src/auth/index.ts
|
|
21
|
+
var auth_exports = {};
|
|
22
|
+
__export(auth_exports, {
|
|
23
|
+
AUTH_EXTRA_FIELD: () => AUTH_EXTRA_FIELD,
|
|
24
|
+
AUTH_FLOW_GLOBAL_KEY: () => AUTH_FLOW_GLOBAL_KEY,
|
|
25
|
+
AUTH_WIRE_FIELDS: () => AUTH_WIRE_FIELDS,
|
|
26
|
+
AuthFlowContext: () => AuthFlowContext,
|
|
27
|
+
AuthFlowProvider: () => AuthFlowProvider,
|
|
28
|
+
AuthPageWrapper: () => AuthPageWrapper,
|
|
29
|
+
CONSENT_SUBMITTED_VALUE: () => CONSENT_SUBMITTED_VALUE,
|
|
30
|
+
DEFAULT_AUTH_MOUNT_ID: () => DEFAULT_AUTH_MOUNT_ID,
|
|
31
|
+
DEFAULT_SUBMIT_METHOD: () => DEFAULT_SUBMIT_METHOD,
|
|
32
|
+
WIRE_TRUE: () => WIRE_TRUE,
|
|
33
|
+
getAddedItems: () => getAddedItems,
|
|
34
|
+
getAuthFlow: () => getAuthFlow,
|
|
35
|
+
mountAuthPage: () => mountAuthPage,
|
|
36
|
+
setAuthNavigator: () => setAuthNavigator,
|
|
37
|
+
submitExtra: () => submitExtra,
|
|
38
|
+
submitFinish: () => submitFinish,
|
|
39
|
+
tryGetAuthFlow: () => tryGetAuthFlow,
|
|
40
|
+
useAddedItems: () => useAddedItems,
|
|
41
|
+
useAuthFlow: () => useAuthFlow,
|
|
42
|
+
useExtraField: () => useExtraField
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(auth_exports);
|
|
45
|
+
|
|
46
|
+
// libs/ui/src/auth/contract.ts
|
|
47
|
+
var AUTH_FLOW_GLOBAL_KEY = "__FRONTMCP_AUTH__";
|
|
48
|
+
var DEFAULT_SUBMIT_METHOD = "GET";
|
|
49
|
+
var AUTH_WIRE_FIELDS = {
|
|
50
|
+
/** Pending authorization id. */
|
|
51
|
+
pendingAuthId: "pending_auth_id",
|
|
52
|
+
/** Anti-CSRF token. */
|
|
53
|
+
csrf: "csrf",
|
|
54
|
+
/** Marks a consent-form submission (distinguishes empty-select from first visit). */
|
|
55
|
+
consentSubmitted: "consent_submitted",
|
|
56
|
+
/** Repeated checkbox field carrying selected tool ids on the consent slot. */
|
|
57
|
+
tools: "tools",
|
|
58
|
+
/** Marks a federated submission. */
|
|
59
|
+
federated: "federated",
|
|
60
|
+
/** Repeated checkbox field carrying selected provider ids on the federated slot. */
|
|
61
|
+
providers: "providers",
|
|
62
|
+
/** Marks an incremental authorization. */
|
|
63
|
+
incremental: "incremental",
|
|
64
|
+
/** Target app id for incremental authorization. */
|
|
65
|
+
appId: "app_id",
|
|
66
|
+
/** Generic action discriminator (e.g. consent `authorize`/`skip`, or an extra name). */
|
|
67
|
+
action: "action"
|
|
68
|
+
};
|
|
69
|
+
var CONSENT_SUBMITTED_VALUE = "1";
|
|
70
|
+
var WIRE_TRUE = "true";
|
|
71
|
+
var AUTH_EXTRA_FIELD = AUTH_WIRE_FIELDS.action;
|
|
72
|
+
var DEFAULT_AUTH_MOUNT_ID = "frontmcp-auth-root";
|
|
73
|
+
|
|
74
|
+
// libs/ui/src/auth/vanilla/auth-flow.ts
|
|
75
|
+
function getGlobalCarrier() {
|
|
76
|
+
if (typeof window !== "undefined") {
|
|
77
|
+
return window;
|
|
78
|
+
}
|
|
79
|
+
return globalThis;
|
|
80
|
+
}
|
|
81
|
+
function getAuthFlow() {
|
|
82
|
+
const state = tryGetAuthFlow();
|
|
83
|
+
if (!state) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`[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.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return state;
|
|
89
|
+
}
|
|
90
|
+
function tryGetAuthFlow() {
|
|
91
|
+
const carrier = getGlobalCarrier();
|
|
92
|
+
const state = carrier?.[AUTH_FLOW_GLOBAL_KEY];
|
|
93
|
+
if (!state || typeof state !== "object") {
|
|
94
|
+
return void 0;
|
|
95
|
+
}
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
function getAddedItems(name) {
|
|
99
|
+
const items = tryGetAuthFlow()?.addedItems?.[name];
|
|
100
|
+
return Array.isArray(items) ? items : [];
|
|
101
|
+
}
|
|
102
|
+
function toEntries(input) {
|
|
103
|
+
if (!input) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
if (isFormElement(input)) {
|
|
107
|
+
return formDataToEntries(new FormData(input));
|
|
108
|
+
}
|
|
109
|
+
if (typeof FormData !== "undefined" && input instanceof FormData) {
|
|
110
|
+
return formDataToEntries(input);
|
|
111
|
+
}
|
|
112
|
+
const entries = [];
|
|
113
|
+
for (const [key, value] of Object.entries(input)) {
|
|
114
|
+
if (value === void 0 || value === null) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
for (const v of value) {
|
|
119
|
+
if (v !== void 0 && v !== null) {
|
|
120
|
+
entries.push([key, String(v)]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
entries.push([key, String(value)]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return entries;
|
|
128
|
+
}
|
|
129
|
+
function formDataToEntries(fd) {
|
|
130
|
+
const entries = [];
|
|
131
|
+
fd.forEach((value, key) => {
|
|
132
|
+
if (typeof value === "string") {
|
|
133
|
+
entries.push([key, value]);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return entries;
|
|
137
|
+
}
|
|
138
|
+
function isFormElement(input) {
|
|
139
|
+
return typeof input === "object" && // `nodeName` + a `FormData`-constructible shape is enough; avoids requiring
|
|
140
|
+
// a live `HTMLFormElement` global (jsdom provides one, node does not).
|
|
141
|
+
input.nodeName === "FORM";
|
|
142
|
+
}
|
|
143
|
+
function buildGetUrl(base, entries) {
|
|
144
|
+
const origin = typeof window !== "undefined" && window.location ? window.location.origin : void 0;
|
|
145
|
+
const url = origin ? new URL(base, origin) : new URL(base, "http://localhost");
|
|
146
|
+
for (const [key, value] of entries) {
|
|
147
|
+
url.searchParams.append(key, value);
|
|
148
|
+
}
|
|
149
|
+
if (!origin && !/^https?:\/\//i.test(base)) {
|
|
150
|
+
return `${url.pathname}${url.search}`;
|
|
151
|
+
}
|
|
152
|
+
return url.toString();
|
|
153
|
+
}
|
|
154
|
+
function buildUrlEncodedBody(entries) {
|
|
155
|
+
const params = new URLSearchParams();
|
|
156
|
+
for (const [key, value] of entries) {
|
|
157
|
+
params.append(key, value);
|
|
158
|
+
}
|
|
159
|
+
return params.toString();
|
|
160
|
+
}
|
|
161
|
+
function withControlFields(state, entries, markers) {
|
|
162
|
+
const present = new Set(entries.map(([k]) => k));
|
|
163
|
+
const merged = [...entries];
|
|
164
|
+
const ensure = (key, value) => {
|
|
165
|
+
if (value !== void 0 && !present.has(key)) {
|
|
166
|
+
merged.push([key, value]);
|
|
167
|
+
present.add(key);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
ensure(AUTH_WIRE_FIELDS.pendingAuthId, state.pendingAuthId);
|
|
171
|
+
ensure(AUTH_WIRE_FIELDS.csrf, state.csrfToken);
|
|
172
|
+
for (const [key, value] of markers) {
|
|
173
|
+
ensure(key, value);
|
|
174
|
+
}
|
|
175
|
+
return merged;
|
|
176
|
+
}
|
|
177
|
+
function finishMarkers(state) {
|
|
178
|
+
switch (state.slot) {
|
|
179
|
+
case "consent":
|
|
180
|
+
return [[AUTH_WIRE_FIELDS.consentSubmitted, CONSENT_SUBMITTED_VALUE]];
|
|
181
|
+
default:
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function dispatch(method, url, entries) {
|
|
186
|
+
if (typeof fetch !== "function") {
|
|
187
|
+
throw new Error("[auth-ui] global fetch is unavailable in this environment");
|
|
188
|
+
}
|
|
189
|
+
if (method === "GET") {
|
|
190
|
+
return fetch(buildGetUrl(url, entries), {
|
|
191
|
+
method: "GET",
|
|
192
|
+
credentials: "same-origin",
|
|
193
|
+
headers: { Accept: "application/json, text/html" }
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return fetch(url, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
credentials: "same-origin",
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
201
|
+
Accept: "application/json, text/html"
|
|
202
|
+
},
|
|
203
|
+
body: buildUrlEncodedBody(entries)
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
var defaultNavigator = (url) => {
|
|
207
|
+
if (typeof window !== "undefined" && window.location) {
|
|
208
|
+
window.location.assign(url);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
var currentNavigator = defaultNavigator;
|
|
212
|
+
function setAuthNavigator(navigator) {
|
|
213
|
+
currentNavigator = navigator ?? defaultNavigator;
|
|
214
|
+
}
|
|
215
|
+
async function submitFinish(formOrData, options = {}) {
|
|
216
|
+
const state = options.state ?? getAuthFlow();
|
|
217
|
+
if (!state.submitUrl) {
|
|
218
|
+
throw new Error("[auth-ui] submitFinish called but the injected flow state has no submitUrl");
|
|
219
|
+
}
|
|
220
|
+
const method = state.submitMethod ?? DEFAULT_SUBMIT_METHOD;
|
|
221
|
+
const entries = withControlFields(state, toEntries(formOrData), finishMarkers(state));
|
|
222
|
+
const response = await dispatch(method, state.submitUrl, entries);
|
|
223
|
+
const shouldNavigate = options.navigate ?? (typeof window !== "undefined" && !!window.location);
|
|
224
|
+
if (shouldNavigate && response.redirected && response.url) {
|
|
225
|
+
currentNavigator(response.url);
|
|
226
|
+
}
|
|
227
|
+
return response;
|
|
228
|
+
}
|
|
229
|
+
async function submitExtra(name, data, state) {
|
|
230
|
+
const flow = state ?? getAuthFlow();
|
|
231
|
+
const target = flow.extraUrl ?? flow.submitUrl;
|
|
232
|
+
if (!target) {
|
|
233
|
+
throw new Error("[auth-ui] submitExtra called but the injected flow state has no extraUrl/submitUrl");
|
|
234
|
+
}
|
|
235
|
+
const markers = flow.extraUrl ? [] : [[AUTH_EXTRA_FIELD, name]];
|
|
236
|
+
const entries = withControlFields(flow, toEntries(data), markers);
|
|
237
|
+
const response = await dispatch("POST", target, entries);
|
|
238
|
+
return parseExtraResponse(response);
|
|
239
|
+
}
|
|
240
|
+
async function parseExtraResponse(response) {
|
|
241
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
242
|
+
if (contentType.includes("application/json")) {
|
|
243
|
+
try {
|
|
244
|
+
const body = await response.json();
|
|
245
|
+
return {
|
|
246
|
+
ok: body.ok ?? response.ok,
|
|
247
|
+
error: typeof body.error === "string" ? body.error : void 0,
|
|
248
|
+
addedItems: body.addedItems && typeof body.addedItems === "object" ? body.addedItems : void 0,
|
|
249
|
+
sideEffects: body.sideEffects && typeof body.sideEffects === "object" ? body.sideEffects : void 0
|
|
250
|
+
};
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (response.ok) {
|
|
255
|
+
return { ok: true };
|
|
256
|
+
}
|
|
257
|
+
return { ok: false, error: `Request failed with status ${response.status}` };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// libs/ui/src/auth/context.tsx
|
|
261
|
+
var import_react = require("react");
|
|
262
|
+
var AuthFlowContext = (0, import_react.createContext)(null);
|
|
263
|
+
function useAuthFlowContext() {
|
|
264
|
+
const ctx = (0, import_react.useContext)(AuthFlowContext);
|
|
265
|
+
if (!ctx) {
|
|
266
|
+
throw new Error("[auth-ui] useAuthFlow* hooks must be used inside <AuthPageWrapper> / <AuthFlowProvider>");
|
|
267
|
+
}
|
|
268
|
+
return ctx;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// libs/ui/src/auth/AuthPageWrapper.tsx
|
|
272
|
+
var import_react2 = require("react");
|
|
273
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
274
|
+
function AuthFlowProvider({
|
|
275
|
+
children,
|
|
276
|
+
state: stateOverride
|
|
277
|
+
}) {
|
|
278
|
+
const injected = stateOverride ?? tryGetAuthFlow();
|
|
279
|
+
const [state, setState] = (0, import_react2.useState)(
|
|
280
|
+
injected ?? { slot: "error", error: "No authorization flow state was provided." }
|
|
281
|
+
);
|
|
282
|
+
const value = (0, import_react2.useMemo)(
|
|
283
|
+
() => ({
|
|
284
|
+
state,
|
|
285
|
+
update: (patch) => setState((prev) => ({ ...prev, ...patch }))
|
|
286
|
+
}),
|
|
287
|
+
[state]
|
|
288
|
+
);
|
|
289
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AuthFlowContext.Provider, { value, children });
|
|
290
|
+
}
|
|
291
|
+
function ControlFields({ state }) {
|
|
292
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
293
|
+
state.pendingAuthId !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "hidden", name: AUTH_WIRE_FIELDS.pendingAuthId, value: state.pendingAuthId }),
|
|
294
|
+
state.csrfToken !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "hidden", name: AUTH_WIRE_FIELDS.csrf, value: state.csrfToken }),
|
|
295
|
+
state.slot === "consent" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { type: "hidden", name: AUTH_WIRE_FIELDS.consentSubmitted, value: CONSENT_SUBMITTED_VALUE })
|
|
296
|
+
] });
|
|
297
|
+
}
|
|
298
|
+
function AuthPageWrapper({
|
|
299
|
+
children,
|
|
300
|
+
state: stateOverride,
|
|
301
|
+
renderForm = true,
|
|
302
|
+
className
|
|
303
|
+
}) {
|
|
304
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AuthFlowProvider, { state: stateOverride, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AuthPageWrapperInner, { renderForm, className, children }) });
|
|
305
|
+
}
|
|
306
|
+
function AuthPageWrapperInner({
|
|
307
|
+
children,
|
|
308
|
+
renderForm,
|
|
309
|
+
className
|
|
310
|
+
}) {
|
|
311
|
+
const ctx = useAuthFlowContextSafe();
|
|
312
|
+
const state = ctx?.state;
|
|
313
|
+
if (!renderForm || !state?.submitUrl) {
|
|
314
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: className ?? "frontmcp-auth-page", children });
|
|
315
|
+
}
|
|
316
|
+
const method = (state.submitMethod ?? DEFAULT_SUBMIT_METHOD).toLowerCase();
|
|
317
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: className ?? "frontmcp-auth-page", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { action: state.submitUrl, method, children: [
|
|
318
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ControlFields, { state }),
|
|
319
|
+
children
|
|
320
|
+
] }) });
|
|
321
|
+
}
|
|
322
|
+
function useAuthFlowContextSafe() {
|
|
323
|
+
return (0, import_react2.useContext)(AuthFlowContext);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// libs/ui/src/auth/hooks.ts
|
|
327
|
+
var import_react3 = require("react");
|
|
328
|
+
function normalizeFormArg(arg) {
|
|
329
|
+
if (!arg) {
|
|
330
|
+
return void 0;
|
|
331
|
+
}
|
|
332
|
+
const maybeEvent = arg;
|
|
333
|
+
if (typeof maybeEvent.preventDefault === "function" && maybeEvent.currentTarget) {
|
|
334
|
+
maybeEvent.preventDefault();
|
|
335
|
+
return maybeEvent.currentTarget;
|
|
336
|
+
}
|
|
337
|
+
return arg;
|
|
338
|
+
}
|
|
339
|
+
function useAuthFlow() {
|
|
340
|
+
const { state } = useAuthFlowContext();
|
|
341
|
+
const submitFinish2 = (0, import_react3.useCallback)(
|
|
342
|
+
(formOrEvent) => submitFinish(normalizeFormArg(formOrEvent), { state }),
|
|
343
|
+
[state]
|
|
344
|
+
);
|
|
345
|
+
return {
|
|
346
|
+
slot: state.slot,
|
|
347
|
+
pendingAuthId: state.pendingAuthId,
|
|
348
|
+
clientName: state.clientName,
|
|
349
|
+
clientId: state.clientId,
|
|
350
|
+
scopes: state.scopes ?? [],
|
|
351
|
+
redirectUri: state.redirectUri,
|
|
352
|
+
resource: state.resource,
|
|
353
|
+
error: state.error,
|
|
354
|
+
providers: state.providers ?? [],
|
|
355
|
+
tools: state.tools ?? [],
|
|
356
|
+
extras: state.extras ?? {},
|
|
357
|
+
state,
|
|
358
|
+
submitFinish: submitFinish2
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function useAddedItems(name) {
|
|
362
|
+
const { state } = useAuthFlowContext();
|
|
363
|
+
const items = state.addedItems?.[name];
|
|
364
|
+
return Array.isArray(items) ? items : [];
|
|
365
|
+
}
|
|
366
|
+
function useExtraField(name) {
|
|
367
|
+
const { state, update } = useAuthFlowContext();
|
|
368
|
+
const [result, setResult] = (0, import_react3.useState)(void 0);
|
|
369
|
+
const [pending, setPending] = (0, import_react3.useState)(false);
|
|
370
|
+
const onSubmit = (0, import_react3.useCallback)(
|
|
371
|
+
async (formOrEvent) => {
|
|
372
|
+
const data = normalizeFormArg(formOrEvent);
|
|
373
|
+
setPending(true);
|
|
374
|
+
try {
|
|
375
|
+
const res = await submitExtra(name, data, state);
|
|
376
|
+
setResult(res);
|
|
377
|
+
if (res.ok && res.addedItems) {
|
|
378
|
+
update({ addedItems: res.addedItems });
|
|
379
|
+
}
|
|
380
|
+
return res;
|
|
381
|
+
} catch (err) {
|
|
382
|
+
const failure = {
|
|
383
|
+
ok: false,
|
|
384
|
+
error: err instanceof Error ? err.message : "Failed to submit field"
|
|
385
|
+
};
|
|
386
|
+
setResult(failure);
|
|
387
|
+
return failure;
|
|
388
|
+
} finally {
|
|
389
|
+
setPending(false);
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
[name, state, update]
|
|
393
|
+
);
|
|
394
|
+
return { onSubmit, result, pending };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// libs/ui/src/auth/hydrate.tsx
|
|
398
|
+
var import_client = require("react-dom/client");
|
|
399
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
400
|
+
function resolveContainer(container) {
|
|
401
|
+
if (container && typeof container !== "string") {
|
|
402
|
+
return container;
|
|
403
|
+
}
|
|
404
|
+
if (typeof document === "undefined") {
|
|
405
|
+
throw new Error("[auth-ui] mountAuthPage requires a DOM (document is undefined)");
|
|
406
|
+
}
|
|
407
|
+
const selector = container ?? `#${DEFAULT_AUTH_MOUNT_ID}`;
|
|
408
|
+
const el = document.querySelector(selector);
|
|
409
|
+
if (!el) {
|
|
410
|
+
throw new Error(`[auth-ui] mountAuthPage could not find a container matching "${selector}"`);
|
|
411
|
+
}
|
|
412
|
+
return el;
|
|
413
|
+
}
|
|
414
|
+
function mountAuthPage(Component, options = {}) {
|
|
415
|
+
const container = resolveContainer(options.container);
|
|
416
|
+
const root = (0, import_client.createRoot)(container);
|
|
417
|
+
root.render(
|
|
418
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AuthPageWrapper, { state: options.state, renderForm: options.renderForm, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Component, {}) })
|
|
419
|
+
);
|
|
420
|
+
return root;
|
|
421
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@frontmcp/ui/auth/vanilla` — framework-free helpers that read the injected
|
|
3
|
+
* {@link AuthFlowState} global and drive the OAuth flow. No React.
|
|
4
|
+
*
|
|
5
|
+
* These are browser-DOM (client-only) primitives the `/auth` React hooks are
|
|
6
|
+
* built on, and are usable directly from a non-React (or no-framework)
|
|
7
|
+
* authorization page. The framework-free CONTRACT they consume is co-located in
|
|
8
|
+
* `@frontmcp/ui/auth` (`./contract`); these helpers touch
|
|
9
|
+
* `window`/`fetch`/`FormData`, so they are browser-only.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
import { type AuthExtraResult, type AuthFlowState, type AuthFormInput } from '../contract';
|
|
14
|
+
/**
|
|
15
|
+
* Read the server-injected {@link AuthFlowState}.
|
|
16
|
+
*
|
|
17
|
+
* @throws Error when no flow state has been injected (the page was not rendered
|
|
18
|
+
* by a FrontMCP auth slot, or the injection script did not run yet). Callers
|
|
19
|
+
* that want a tolerant read should use {@link tryGetAuthFlow}.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getAuthFlow(): AuthFlowState;
|
|
22
|
+
/**
|
|
23
|
+
* Read the server-injected {@link AuthFlowState}, or `undefined` when it is not
|
|
24
|
+
* present. Does not throw — useful for optional/SSR-safe reads.
|
|
25
|
+
*/
|
|
26
|
+
export declare function tryGetAuthFlow(): AuthFlowState | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Return the server-side accumulator for a named multi-step input (e.g. the
|
|
29
|
+
* `envs` a user has added so far). Empty array when the name is absent.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getAddedItems<T = unknown>(name: string): T[];
|
|
32
|
+
/**
|
|
33
|
+
* Options for {@link submitFinish}.
|
|
34
|
+
*/
|
|
35
|
+
export interface SubmitFinishOptions {
|
|
36
|
+
/**
|
|
37
|
+
* When true (the default in a real browser), a non-redirected/non-JSON HTML
|
|
38
|
+
* response causes a full navigation to `response.url` (the OAuth code
|
|
39
|
+
* redirect). Set false to suppress navigation and inspect the {@link Response}
|
|
40
|
+
* yourself (used by tests and SPA-style pages).
|
|
41
|
+
*/
|
|
42
|
+
navigate?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Drive the submit from this explicit flow state instead of reading the
|
|
45
|
+
* injected `window.__FRONTMCP_AUTH__`. Used by the React hooks (which already
|
|
46
|
+
* hold the state via context); standalone callers omit it.
|
|
47
|
+
*/
|
|
48
|
+
state?: AuthFlowState;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* How {@link submitFinish} performs a top-level navigation to the OAuth
|
|
52
|
+
* redirect. Defaults to `window.location.assign`. Exposed so SPA-style pages
|
|
53
|
+
* (and tests) can override it via {@link setAuthNavigator} without fighting a
|
|
54
|
+
* non-configurable `window.location`.
|
|
55
|
+
*/
|
|
56
|
+
export type AuthNavigator = (url: string) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Override the navigation function used to follow the OAuth redirect after a
|
|
59
|
+
* successful {@link submitFinish}. Pass no argument to reset to the default
|
|
60
|
+
* (`window.location.assign`).
|
|
61
|
+
*/
|
|
62
|
+
export declare function setAuthNavigator(navigator?: AuthNavigator): void;
|
|
63
|
+
/**
|
|
64
|
+
* Finish the authorization flow: submit identity / selection back to the
|
|
65
|
+
* server's callback (`submitUrl`), carrying `pending_auth_id`, `csrf`, and the
|
|
66
|
+
* slot's marker fields (e.g. `consent_submitted=1` for consent).
|
|
67
|
+
*
|
|
68
|
+
* On success the server responds with the OAuth redirect to the client's
|
|
69
|
+
* `redirect_uri`; in a browser this helper follows it via a full navigation
|
|
70
|
+
* unless {@link SubmitFinishOptions.navigate} is false.
|
|
71
|
+
*
|
|
72
|
+
* @param formOrData The developer's form/fields (identity inputs, selected
|
|
73
|
+
* `tools`/`providers` checkboxes, etc.).
|
|
74
|
+
* @param options Submit options. `options.state` lets a caller (e.g. the React
|
|
75
|
+
* hooks) drive the submit from an explicit flow state instead of re-reading the
|
|
76
|
+
* injected global; when omitted the injected `window.__FRONTMCP_AUTH__` is used.
|
|
77
|
+
* @returns The raw {@link Response} (so callers can branch on a re-rendered
|
|
78
|
+
* error page vs. a redirect).
|
|
79
|
+
*/
|
|
80
|
+
export declare function submitFinish(formOrData?: AuthFormInput, options?: SubmitFinishOptions): Promise<Response>;
|
|
81
|
+
/**
|
|
82
|
+
* Submit a single validated extra field (a `@AuthExtra(name)` round-trip).
|
|
83
|
+
*
|
|
84
|
+
* Posts the field data plus `pending_auth_id` + `csrf` to `extraUrl` when the
|
|
85
|
+
* server provided one, otherwise to `submitUrl` with an `action=<name>` field
|
|
86
|
+
* so the server can route it. Always uses POST (extras mutate server state —
|
|
87
|
+
* unlike the GET finish round-trip).
|
|
88
|
+
*
|
|
89
|
+
* @param name The extra's name (must match the server's `@AuthExtra(name)`).
|
|
90
|
+
* @param data The field value(s) to validate/add.
|
|
91
|
+
* @param state Optional explicit flow state (the React hooks pass their context
|
|
92
|
+
* state); when omitted the injected `window.__FRONTMCP_AUTH__` is used.
|
|
93
|
+
* @returns A parsed {@link AuthExtraResult} (`{ ok, error?, addedItems? }`).
|
|
94
|
+
*/
|
|
95
|
+
export declare function submitExtra(name: string, data?: AuthFormInput, state?: AuthFlowState): Promise<AuthExtraResult>;
|
|
96
|
+
//# sourceMappingURL=auth-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-flow.d.ts","sourceRoot":"","sources":["../../../src/auth/vanilla/auth-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,aAAa,EAEnB,MAAM,aAAa,CAAC;AAgBrB;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,aAAa,CAS3C;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,aAAa,GAAG,SAAS,CAO1D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,EAAE,CAG5D;AAwJD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;AAUlD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,aAAa,GAAG,IAAI,CAEhE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,YAAY,CAAC,UAAU,CAAC,EAAE,aAAa,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAenH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,CAarH"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@frontmcp/ui/auth/vanilla` — framework-free (browser-DOM) flow helpers + the
|
|
3
|
+
* contract.
|
|
4
|
+
*
|
|
5
|
+
* Re-exports the contract (the single source of truth lives in
|
|
6
|
+
* `@frontmcp/ui/auth`) so a non-React page can
|
|
7
|
+
* `import { getAuthFlow, type AuthFlowState } from '@frontmcp/ui/auth/vanilla'`
|
|
8
|
+
* from a single entrypoint.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
export * from '../contract';
|
|
13
|
+
export * from './auth-flow';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/auth/vanilla/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC"}
|