@foir/sdk 0.1.0 → 0.3.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 +72 -0
- package/dist/next.d.ts +178 -0
- package/dist/next.js +275 -0
- package/dist/push.d.ts +109 -0
- package/dist/push.js +259 -0
- package/dist/react.d.ts +244 -0
- package/dist/react.js +608 -0
- package/dist/server.d.ts +78 -1
- package/dist/server.js +63 -0
- package/package.json +13 -1
package/dist/react.js
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/react/FoirProvider.tsx
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState
|
|
12
|
+
} from "react";
|
|
13
|
+
|
|
14
|
+
// src/graphql/foir-request.ts
|
|
15
|
+
import { print } from "graphql";
|
|
16
|
+
function isFoirPreview(env) {
|
|
17
|
+
return env.FOIR_PREVIEW === "true";
|
|
18
|
+
}
|
|
19
|
+
var FoirRequestError = class extends Error {
|
|
20
|
+
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
21
|
+
status;
|
|
22
|
+
/** GraphQL errors, when the request failed with a no-data error envelope. */
|
|
23
|
+
graphQLErrors;
|
|
24
|
+
cause;
|
|
25
|
+
constructor(message, options) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "FoirRequestError";
|
|
28
|
+
this.cause = options?.cause;
|
|
29
|
+
this.status = options?.status;
|
|
30
|
+
this.graphQLErrors = options?.graphQLErrors;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
async function foirRequest(env, document, variables, options) {
|
|
34
|
+
const endpoint = env.PLATFORM_API_URL;
|
|
35
|
+
const apiKey = isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
|
|
36
|
+
if (!endpoint || !apiKey) {
|
|
37
|
+
throw new FoirRequestError(
|
|
38
|
+
"[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const headers = {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"x-api-key": apiKey
|
|
44
|
+
};
|
|
45
|
+
if (isFoirPreview(env)) {
|
|
46
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
47
|
+
}
|
|
48
|
+
if (options?.bearerToken) {
|
|
49
|
+
headers["Authorization"] = `Bearer ${options.bearerToken}`;
|
|
50
|
+
}
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetch(endpoint, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers,
|
|
56
|
+
body: JSON.stringify({ query: print(document), variables })
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new FoirRequestError("[foir] network error", { cause: err });
|
|
60
|
+
}
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
throw new FoirRequestError(`[foir] HTTP ${res.status} ${res.statusText}`, {
|
|
63
|
+
status: res.status
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const json = await res.json();
|
|
67
|
+
if (json.errors?.length) {
|
|
68
|
+
const msg = json.errors.map((e) => e.message).join("; ");
|
|
69
|
+
if (json.data) {
|
|
70
|
+
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
71
|
+
} else {
|
|
72
|
+
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
73
|
+
graphQLErrors: json.errors
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!json.data) {
|
|
78
|
+
throw new FoirRequestError("[foir] empty GraphQL response");
|
|
79
|
+
}
|
|
80
|
+
return json.data;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/auth/operations.ts
|
|
84
|
+
import { parse } from "graphql";
|
|
85
|
+
var AUTH_RESULT_FIELDS = "token refreshToken customerId email";
|
|
86
|
+
var CustomerLoginDocument = parse(
|
|
87
|
+
`mutation CustomerLogin($email: String!, $password: String!) {
|
|
88
|
+
customerLogin(email: $email, password: $password) { ${AUTH_RESULT_FIELDS} }
|
|
89
|
+
}`
|
|
90
|
+
);
|
|
91
|
+
var CustomerRegisterDocument = parse(
|
|
92
|
+
`mutation CustomerRegister($email: String!, $password: String!) {
|
|
93
|
+
customerRegister(email: $email, password: $password) { ${AUTH_RESULT_FIELDS} }
|
|
94
|
+
}`
|
|
95
|
+
);
|
|
96
|
+
var CustomerRefreshTokenDocument = parse(
|
|
97
|
+
`mutation CustomerRefreshToken($refreshToken: String!) {
|
|
98
|
+
customerRefreshToken(refreshToken: $refreshToken) { ${AUTH_RESULT_FIELDS} }
|
|
99
|
+
}`
|
|
100
|
+
);
|
|
101
|
+
var CustomerRequestOTPDocument = parse(
|
|
102
|
+
`mutation CustomerRequestOTP($email: String!) {
|
|
103
|
+
customerRequestOTP(email: $email)
|
|
104
|
+
}`
|
|
105
|
+
);
|
|
106
|
+
var CustomerLoginOTPDocument = parse(
|
|
107
|
+
`mutation CustomerLoginOTP($email: String!, $otp: String!) {
|
|
108
|
+
customerLoginOTP(email: $email, otp: $otp) { ${AUTH_RESULT_FIELDS} }
|
|
109
|
+
}`
|
|
110
|
+
);
|
|
111
|
+
var CustomerLoginWithProviderDocument = parse(
|
|
112
|
+
`mutation CustomerLoginWithProvider($input: ProviderLoginInput!) {
|
|
113
|
+
customerLoginWithProvider(input: $input) { redirectUrl token }
|
|
114
|
+
}`
|
|
115
|
+
);
|
|
116
|
+
var CustomerProviderCallbackDocument = parse(
|
|
117
|
+
`mutation CustomerProviderCallback($input: ProviderCallbackInput!) {
|
|
118
|
+
customerProviderCallback(input: $input) { ${AUTH_RESULT_FIELDS} }
|
|
119
|
+
}`
|
|
120
|
+
);
|
|
121
|
+
var CustomerProviderVerifyOTPDocument = parse(
|
|
122
|
+
`mutation CustomerProviderVerifyOTP($input: ProviderOTPVerifyInput!) {
|
|
123
|
+
customerProviderVerifyOTP(input: $input) { ${AUTH_RESULT_FIELDS} }
|
|
124
|
+
}`
|
|
125
|
+
);
|
|
126
|
+
var AuthProvidersDocument = parse(
|
|
127
|
+
`query AuthProviders {
|
|
128
|
+
authProviders { key name type isDefault }
|
|
129
|
+
}`
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// src/auth/auth.ts
|
|
133
|
+
var TOKEN_KEY = "foir:token";
|
|
134
|
+
var REFRESH_KEY = "foir:refreshToken";
|
|
135
|
+
var CUSTOMER_ID_KEY = "foir:customerId";
|
|
136
|
+
var EMAIL_KEY = "foir:email";
|
|
137
|
+
async function customerLogin(env, vars) {
|
|
138
|
+
return (await foirRequest(env, CustomerLoginDocument, vars)).customerLogin;
|
|
139
|
+
}
|
|
140
|
+
async function customerRegister(env, vars) {
|
|
141
|
+
return (await foirRequest(env, CustomerRegisterDocument, vars)).customerRegister;
|
|
142
|
+
}
|
|
143
|
+
async function customerRequestOTP(env, email) {
|
|
144
|
+
return (await foirRequest(env, CustomerRequestOTPDocument, { email })).customerRequestOTP;
|
|
145
|
+
}
|
|
146
|
+
async function customerLoginOTP(env, vars) {
|
|
147
|
+
return (await foirRequest(env, CustomerLoginOTPDocument, vars)).customerLoginOTP;
|
|
148
|
+
}
|
|
149
|
+
async function customerRefreshToken(env, refreshToken) {
|
|
150
|
+
return (await foirRequest(env, CustomerRefreshTokenDocument, { refreshToken })).customerRefreshToken;
|
|
151
|
+
}
|
|
152
|
+
async function customerLoginWithProvider(env, input) {
|
|
153
|
+
return (await foirRequest(env, CustomerLoginWithProviderDocument, { input })).customerLoginWithProvider;
|
|
154
|
+
}
|
|
155
|
+
async function customerProviderCallback(env, input) {
|
|
156
|
+
return (await foirRequest(env, CustomerProviderCallbackDocument, { input })).customerProviderCallback;
|
|
157
|
+
}
|
|
158
|
+
async function getAuthProviders(env) {
|
|
159
|
+
return (await foirRequest(env, AuthProvidersDocument, {})).authProviders;
|
|
160
|
+
}
|
|
161
|
+
async function saveSession(adapter, result) {
|
|
162
|
+
await adapter.set(TOKEN_KEY, result.token);
|
|
163
|
+
await adapter.set(REFRESH_KEY, result.refreshToken);
|
|
164
|
+
await adapter.set(CUSTOMER_ID_KEY, result.customerId);
|
|
165
|
+
await adapter.set(EMAIL_KEY, result.email);
|
|
166
|
+
return { id: result.customerId, email: result.email };
|
|
167
|
+
}
|
|
168
|
+
async function clearSession(adapter) {
|
|
169
|
+
await adapter.unset(TOKEN_KEY);
|
|
170
|
+
await adapter.unset(REFRESH_KEY);
|
|
171
|
+
await adapter.unset(CUSTOMER_ID_KEY);
|
|
172
|
+
await adapter.unset(EMAIL_KEY);
|
|
173
|
+
}
|
|
174
|
+
async function getSessionToken(adapter) {
|
|
175
|
+
return await adapter.get(TOKEN_KEY) ?? null;
|
|
176
|
+
}
|
|
177
|
+
async function getSessionCustomer(adapter) {
|
|
178
|
+
const id = await adapter.get(CUSTOMER_ID_KEY) ?? null;
|
|
179
|
+
const email = await adapter.get(EMAIL_KEY) ?? null;
|
|
180
|
+
if (!id) return null;
|
|
181
|
+
return { id, email: email ?? "" };
|
|
182
|
+
}
|
|
183
|
+
async function loginWithPassword(adapter, env, vars) {
|
|
184
|
+
return saveSession(adapter, await customerLogin(env, vars));
|
|
185
|
+
}
|
|
186
|
+
async function registerWithPassword(adapter, env, vars) {
|
|
187
|
+
return saveSession(adapter, await customerRegister(env, vars));
|
|
188
|
+
}
|
|
189
|
+
async function loginWithOTP(adapter, env, vars) {
|
|
190
|
+
return saveSession(adapter, await customerLoginOTP(env, vars));
|
|
191
|
+
}
|
|
192
|
+
async function refreshSession(adapter, env) {
|
|
193
|
+
const refreshToken = await adapter.get(REFRESH_KEY) ?? null;
|
|
194
|
+
if (!refreshToken) return null;
|
|
195
|
+
try {
|
|
196
|
+
const result = await customerRefreshToken(env, refreshToken);
|
|
197
|
+
await saveSession(adapter, result);
|
|
198
|
+
return { token: result.token, refreshToken: result.refreshToken };
|
|
199
|
+
} catch {
|
|
200
|
+
await clearSession(adapter);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/react/localStorageAdapter.ts
|
|
206
|
+
function createLocalStorageAdapter() {
|
|
207
|
+
const store = () => {
|
|
208
|
+
try {
|
|
209
|
+
return typeof window !== "undefined" ? window.localStorage : null;
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
get: (key) => store()?.getItem(key) ?? null,
|
|
216
|
+
set: (key, value) => {
|
|
217
|
+
store()?.setItem(key, value);
|
|
218
|
+
},
|
|
219
|
+
unset: (key) => {
|
|
220
|
+
store()?.removeItem(key);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/react/FoirProvider.tsx
|
|
226
|
+
import { jsx } from "react/jsx-runtime";
|
|
227
|
+
var FoirContext = createContext(null);
|
|
228
|
+
function FoirProvider({ env, adapter, children }) {
|
|
229
|
+
const envRef = useRef(env);
|
|
230
|
+
envRef.current = env;
|
|
231
|
+
const adapterRef = useRef(adapter ?? createLocalStorageAdapter());
|
|
232
|
+
if (adapter && adapter !== adapterRef.current) adapterRef.current = adapter;
|
|
233
|
+
const [status, setStatus] = useState("loading");
|
|
234
|
+
const [customer, setCustomer] = useState(null);
|
|
235
|
+
const apply = useCallback((c) => {
|
|
236
|
+
setCustomer(c);
|
|
237
|
+
setStatus(c ? "authenticated" : "unauthenticated");
|
|
238
|
+
}, []);
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
let cancelled = false;
|
|
241
|
+
void (async () => {
|
|
242
|
+
const c = await getSessionCustomer(adapterRef.current);
|
|
243
|
+
if (!cancelled) apply(c);
|
|
244
|
+
})();
|
|
245
|
+
return () => {
|
|
246
|
+
cancelled = true;
|
|
247
|
+
};
|
|
248
|
+
}, [apply]);
|
|
249
|
+
const signIn = useCallback(
|
|
250
|
+
async (vars) => {
|
|
251
|
+
const c = await loginWithPassword(adapterRef.current, envRef.current, vars);
|
|
252
|
+
apply(c);
|
|
253
|
+
return c;
|
|
254
|
+
},
|
|
255
|
+
[apply]
|
|
256
|
+
);
|
|
257
|
+
const register = useCallback(
|
|
258
|
+
async (vars) => {
|
|
259
|
+
const c = await registerWithPassword(adapterRef.current, envRef.current, vars);
|
|
260
|
+
apply(c);
|
|
261
|
+
return c;
|
|
262
|
+
},
|
|
263
|
+
[apply]
|
|
264
|
+
);
|
|
265
|
+
const requestOtp = useCallback(
|
|
266
|
+
(email) => customerRequestOTP(envRef.current, email),
|
|
267
|
+
[]
|
|
268
|
+
);
|
|
269
|
+
const signInWithOtp = useCallback(
|
|
270
|
+
async (vars) => {
|
|
271
|
+
const c = await loginWithOTP(adapterRef.current, envRef.current, vars);
|
|
272
|
+
apply(c);
|
|
273
|
+
return c;
|
|
274
|
+
},
|
|
275
|
+
[apply]
|
|
276
|
+
);
|
|
277
|
+
const signInWithProvider = useCallback(
|
|
278
|
+
async (providerKey, opts) => {
|
|
279
|
+
const res = await customerLoginWithProvider(envRef.current, {
|
|
280
|
+
providerKey,
|
|
281
|
+
...opts
|
|
282
|
+
});
|
|
283
|
+
if (res.redirectUrl && typeof window !== "undefined") {
|
|
284
|
+
window.location.assign(res.redirectUrl);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
[]
|
|
288
|
+
);
|
|
289
|
+
const completeProviderLogin = useCallback(
|
|
290
|
+
async (input) => {
|
|
291
|
+
const result = await customerProviderCallback(envRef.current, input);
|
|
292
|
+
const c = await saveSession(adapterRef.current, result);
|
|
293
|
+
apply(c);
|
|
294
|
+
return c;
|
|
295
|
+
},
|
|
296
|
+
[apply]
|
|
297
|
+
);
|
|
298
|
+
const signOut = useCallback(async () => {
|
|
299
|
+
await clearSession(adapterRef.current);
|
|
300
|
+
apply(null);
|
|
301
|
+
}, [apply]);
|
|
302
|
+
const refresh = useCallback(async () => {
|
|
303
|
+
const tokens = await refreshSession(adapterRef.current, envRef.current);
|
|
304
|
+
if (!tokens) {
|
|
305
|
+
apply(null);
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return true;
|
|
309
|
+
}, [apply]);
|
|
310
|
+
const getToken = useCallback(() => getSessionToken(adapterRef.current), []);
|
|
311
|
+
const getProviders = useCallback(() => getAuthProviders(envRef.current), []);
|
|
312
|
+
const value = useMemo(
|
|
313
|
+
() => ({
|
|
314
|
+
status,
|
|
315
|
+
customer,
|
|
316
|
+
env,
|
|
317
|
+
adapter: adapterRef.current,
|
|
318
|
+
signIn,
|
|
319
|
+
register,
|
|
320
|
+
requestOtp,
|
|
321
|
+
signInWithOtp,
|
|
322
|
+
signInWithProvider,
|
|
323
|
+
completeProviderLogin,
|
|
324
|
+
signOut,
|
|
325
|
+
refresh,
|
|
326
|
+
getToken,
|
|
327
|
+
getProviders
|
|
328
|
+
}),
|
|
329
|
+
[
|
|
330
|
+
status,
|
|
331
|
+
customer,
|
|
332
|
+
env,
|
|
333
|
+
signIn,
|
|
334
|
+
register,
|
|
335
|
+
requestOtp,
|
|
336
|
+
signInWithOtp,
|
|
337
|
+
signInWithProvider,
|
|
338
|
+
completeProviderLogin,
|
|
339
|
+
signOut,
|
|
340
|
+
refresh,
|
|
341
|
+
getToken,
|
|
342
|
+
getProviders
|
|
343
|
+
]
|
|
344
|
+
);
|
|
345
|
+
return /* @__PURE__ */ jsx(FoirContext.Provider, { value, children });
|
|
346
|
+
}
|
|
347
|
+
function useFoirContext() {
|
|
348
|
+
const ctx = useContext(FoirContext);
|
|
349
|
+
if (!ctx) {
|
|
350
|
+
throw new Error("Foir hooks must be used inside a <FoirProvider>.");
|
|
351
|
+
}
|
|
352
|
+
return ctx;
|
|
353
|
+
}
|
|
354
|
+
function useFoirSession() {
|
|
355
|
+
const { status, customer } = useFoirContext();
|
|
356
|
+
return {
|
|
357
|
+
status,
|
|
358
|
+
customer,
|
|
359
|
+
isLoading: status === "loading",
|
|
360
|
+
isAuthenticated: status === "authenticated"
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function useFoirUser() {
|
|
364
|
+
return useFoirContext().customer;
|
|
365
|
+
}
|
|
366
|
+
function useFoirAuth() {
|
|
367
|
+
const {
|
|
368
|
+
signIn,
|
|
369
|
+
register,
|
|
370
|
+
requestOtp,
|
|
371
|
+
signInWithOtp,
|
|
372
|
+
signInWithProvider,
|
|
373
|
+
completeProviderLogin,
|
|
374
|
+
signOut,
|
|
375
|
+
refresh,
|
|
376
|
+
getToken,
|
|
377
|
+
getProviders
|
|
378
|
+
} = useFoirContext();
|
|
379
|
+
return {
|
|
380
|
+
signIn,
|
|
381
|
+
register,
|
|
382
|
+
requestOtp,
|
|
383
|
+
signInWithOtp,
|
|
384
|
+
signInWithProvider,
|
|
385
|
+
completeProviderLogin,
|
|
386
|
+
signOut,
|
|
387
|
+
refresh,
|
|
388
|
+
getToken,
|
|
389
|
+
getProviders
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/react/SignIn.tsx
|
|
394
|
+
import { useState as useState3 } from "react";
|
|
395
|
+
|
|
396
|
+
// src/react/OAuthButtons.tsx
|
|
397
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
398
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
399
|
+
var REDIRECT_TYPES = /* @__PURE__ */ new Set(["OAUTH2", "OIDC", "SAML"]);
|
|
400
|
+
var rowStyle = { display: "flex", flexDirection: "column", gap: 8 };
|
|
401
|
+
var btnStyle = {
|
|
402
|
+
padding: "8px 12px",
|
|
403
|
+
borderRadius: 6,
|
|
404
|
+
border: "1px solid currentColor",
|
|
405
|
+
background: "transparent",
|
|
406
|
+
cursor: "pointer",
|
|
407
|
+
font: "inherit"
|
|
408
|
+
};
|
|
409
|
+
function OAuthButtons({
|
|
410
|
+
returnTo,
|
|
411
|
+
redirectUri,
|
|
412
|
+
filter,
|
|
413
|
+
children,
|
|
414
|
+
className,
|
|
415
|
+
onError
|
|
416
|
+
}) {
|
|
417
|
+
const { getProviders, signInWithProvider } = useFoirAuth();
|
|
418
|
+
const [providers, setProviders] = useState2([]);
|
|
419
|
+
useEffect2(() => {
|
|
420
|
+
let cancelled = false;
|
|
421
|
+
void getProviders().then((list) => {
|
|
422
|
+
if (!cancelled) setProviders(list);
|
|
423
|
+
}).catch((err) => onError?.(err));
|
|
424
|
+
return () => {
|
|
425
|
+
cancelled = true;
|
|
426
|
+
};
|
|
427
|
+
}, [getProviders, onError]);
|
|
428
|
+
const shown = providers.filter(
|
|
429
|
+
(p) => filter ? filter(p) : REDIRECT_TYPES.has(p.type)
|
|
430
|
+
);
|
|
431
|
+
if (shown.length === 0) return null;
|
|
432
|
+
const start = (p) => {
|
|
433
|
+
void signInWithProvider(p.key, { returnTo, redirectUri }).catch(
|
|
434
|
+
(err) => onError?.(err)
|
|
435
|
+
);
|
|
436
|
+
};
|
|
437
|
+
return /* @__PURE__ */ jsx2("div", { className, "data-foir-oauth-buttons": true, style: className ? void 0 : rowStyle, children: shown.map(
|
|
438
|
+
(p) => children ? /* @__PURE__ */ jsx2("span", { children: children(p, () => start(p)) }, p.key) : /* @__PURE__ */ jsxs(
|
|
439
|
+
"button",
|
|
440
|
+
{
|
|
441
|
+
type: "button",
|
|
442
|
+
"data-foir-provider": p.key,
|
|
443
|
+
onClick: () => start(p),
|
|
444
|
+
style: className ? void 0 : btnStyle,
|
|
445
|
+
children: [
|
|
446
|
+
"Continue with ",
|
|
447
|
+
p.name
|
|
448
|
+
]
|
|
449
|
+
},
|
|
450
|
+
p.key
|
|
451
|
+
)
|
|
452
|
+
) });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/react/SignIn.tsx
|
|
456
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
457
|
+
var formStyle = { display: "flex", flexDirection: "column", gap: 10, maxWidth: 320 };
|
|
458
|
+
var inputStyle = {
|
|
459
|
+
padding: "8px 10px",
|
|
460
|
+
borderRadius: 6,
|
|
461
|
+
border: "1px solid currentColor",
|
|
462
|
+
background: "transparent",
|
|
463
|
+
font: "inherit"
|
|
464
|
+
};
|
|
465
|
+
var submitStyle = { ...inputStyle, cursor: "pointer", fontWeight: 600 };
|
|
466
|
+
var errorStyle = { color: "#c0392b", fontSize: 13, margin: 0 };
|
|
467
|
+
var dividerStyle = { fontSize: 12, opacity: 0.6, textAlign: "center" };
|
|
468
|
+
function SignIn({
|
|
469
|
+
method = "password",
|
|
470
|
+
hostedUrl,
|
|
471
|
+
oauth = true,
|
|
472
|
+
afterSignInUrl,
|
|
473
|
+
onSignIn,
|
|
474
|
+
className
|
|
475
|
+
}) {
|
|
476
|
+
const { signIn, requestOtp, signInWithOtp } = useFoirAuth();
|
|
477
|
+
const [email, setEmail] = useState3("");
|
|
478
|
+
const [password, setPassword] = useState3("");
|
|
479
|
+
const [otp, setOtp] = useState3("");
|
|
480
|
+
const [otpSent, setOtpSent] = useState3(false);
|
|
481
|
+
const [busy, setBusy] = useState3(false);
|
|
482
|
+
const [error, setError] = useState3(null);
|
|
483
|
+
const styled = !className;
|
|
484
|
+
if (hostedUrl) {
|
|
485
|
+
return /* @__PURE__ */ jsx3("div", { className, "data-foir-signin": true, children: /* @__PURE__ */ jsx3("a", { href: hostedUrl, "data-foir-hosted-login": true, style: styled ? submitStyle : void 0, children: "Sign in" }) });
|
|
486
|
+
}
|
|
487
|
+
const finish = (customer) => {
|
|
488
|
+
onSignIn?.(customer);
|
|
489
|
+
if (afterSignInUrl && typeof window !== "undefined") {
|
|
490
|
+
window.location.assign(afterSignInUrl);
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
const onSubmit = async (e) => {
|
|
494
|
+
e.preventDefault();
|
|
495
|
+
setError(null);
|
|
496
|
+
setBusy(true);
|
|
497
|
+
try {
|
|
498
|
+
if (method === "otp") {
|
|
499
|
+
if (!otpSent) {
|
|
500
|
+
await requestOtp(email);
|
|
501
|
+
setOtpSent(true);
|
|
502
|
+
} else {
|
|
503
|
+
finish(await signInWithOtp({ email, otp }));
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
finish(await signIn({ email, password }));
|
|
507
|
+
}
|
|
508
|
+
} catch (err) {
|
|
509
|
+
setError(err instanceof Error ? err.message : "Sign-in failed");
|
|
510
|
+
} finally {
|
|
511
|
+
setBusy(false);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
return /* @__PURE__ */ jsxs2("div", { className, "data-foir-signin": true, style: styled ? formStyle : void 0, children: [
|
|
515
|
+
/* @__PURE__ */ jsxs2("form", { onSubmit, style: styled ? formStyle : void 0, children: [
|
|
516
|
+
/* @__PURE__ */ jsx3(
|
|
517
|
+
"input",
|
|
518
|
+
{
|
|
519
|
+
type: "email",
|
|
520
|
+
name: "email",
|
|
521
|
+
autoComplete: "email",
|
|
522
|
+
placeholder: "Email",
|
|
523
|
+
required: true,
|
|
524
|
+
value: email,
|
|
525
|
+
onChange: (e) => setEmail(e.target.value),
|
|
526
|
+
disabled: busy || method === "otp" && otpSent,
|
|
527
|
+
style: styled ? inputStyle : void 0
|
|
528
|
+
}
|
|
529
|
+
),
|
|
530
|
+
method === "password" && /* @__PURE__ */ jsx3(
|
|
531
|
+
"input",
|
|
532
|
+
{
|
|
533
|
+
type: "password",
|
|
534
|
+
name: "password",
|
|
535
|
+
autoComplete: "current-password",
|
|
536
|
+
placeholder: "Password",
|
|
537
|
+
required: true,
|
|
538
|
+
value: password,
|
|
539
|
+
onChange: (e) => setPassword(e.target.value),
|
|
540
|
+
disabled: busy,
|
|
541
|
+
style: styled ? inputStyle : void 0
|
|
542
|
+
}
|
|
543
|
+
),
|
|
544
|
+
method === "otp" && otpSent && /* @__PURE__ */ jsx3(
|
|
545
|
+
"input",
|
|
546
|
+
{
|
|
547
|
+
type: "text",
|
|
548
|
+
name: "otp",
|
|
549
|
+
inputMode: "numeric",
|
|
550
|
+
autoComplete: "one-time-code",
|
|
551
|
+
placeholder: "One-time code",
|
|
552
|
+
required: true,
|
|
553
|
+
value: otp,
|
|
554
|
+
onChange: (e) => setOtp(e.target.value),
|
|
555
|
+
disabled: busy,
|
|
556
|
+
style: styled ? inputStyle : void 0
|
|
557
|
+
}
|
|
558
|
+
),
|
|
559
|
+
error && /* @__PURE__ */ jsx3("p", { "data-foir-error": true, style: styled ? errorStyle : void 0, children: error }),
|
|
560
|
+
/* @__PURE__ */ jsx3("button", { type: "submit", disabled: busy, style: styled ? submitStyle : void 0, children: busy ? "Please wait\u2026" : method === "otp" ? otpSent ? "Verify code" : "Send code" : "Sign in" })
|
|
561
|
+
] }),
|
|
562
|
+
oauth && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
563
|
+
/* @__PURE__ */ jsx3("div", { "data-foir-divider": true, style: styled ? dividerStyle : void 0, children: "or" }),
|
|
564
|
+
/* @__PURE__ */ jsx3(OAuthButtons, { returnTo: afterSignInUrl, onError: (err) => setError(err instanceof Error ? err.message : "Sign-in failed") })
|
|
565
|
+
] })
|
|
566
|
+
] });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/react/UserButton.tsx
|
|
570
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
571
|
+
var rootStyle = { display: "flex", alignItems: "center", gap: 8 };
|
|
572
|
+
var emailStyle = { fontSize: 14 };
|
|
573
|
+
var btnStyle2 = {
|
|
574
|
+
padding: "4px 10px",
|
|
575
|
+
borderRadius: 6,
|
|
576
|
+
border: "1px solid currentColor",
|
|
577
|
+
background: "transparent",
|
|
578
|
+
cursor: "pointer",
|
|
579
|
+
font: "inherit",
|
|
580
|
+
fontSize: 13
|
|
581
|
+
};
|
|
582
|
+
function UserButton({ afterSignOut, afterSignOutUrl, className }) {
|
|
583
|
+
const user = useFoirUser();
|
|
584
|
+
const { signOut } = useFoirAuth();
|
|
585
|
+
if (!user) return null;
|
|
586
|
+
const styled = !className;
|
|
587
|
+
const onSignOut = async () => {
|
|
588
|
+
await signOut();
|
|
589
|
+
afterSignOut?.();
|
|
590
|
+
if (afterSignOutUrl && typeof window !== "undefined") {
|
|
591
|
+
window.location.assign(afterSignOutUrl);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
return /* @__PURE__ */ jsxs3("div", { className, "data-foir-user-button": true, style: styled ? rootStyle : void 0, children: [
|
|
595
|
+
/* @__PURE__ */ jsx4("span", { "data-foir-user-email": true, style: styled ? emailStyle : void 0, children: user.email }),
|
|
596
|
+
/* @__PURE__ */ jsx4("button", { type: "button", onClick: onSignOut, style: styled ? btnStyle2 : void 0, children: "Sign out" })
|
|
597
|
+
] });
|
|
598
|
+
}
|
|
599
|
+
export {
|
|
600
|
+
FoirProvider,
|
|
601
|
+
OAuthButtons,
|
|
602
|
+
SignIn,
|
|
603
|
+
UserButton,
|
|
604
|
+
createLocalStorageAdapter,
|
|
605
|
+
useFoirAuth,
|
|
606
|
+
useFoirSession,
|
|
607
|
+
useFoirUser
|
|
608
|
+
};
|
package/dist/server.d.ts
CHANGED
|
@@ -226,6 +226,83 @@ declare function verifyScopedToken(token: string, jwksUrl: string, options?: Ver
|
|
|
226
226
|
*/
|
|
227
227
|
declare function parseContextHeader(raw: string | null): Record<string, string>;
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* requireScopedToken — the one-line ingress guard for BFF routes the
|
|
231
|
+
* platform dispatches operations to (EID-498).
|
|
232
|
+
*
|
|
233
|
+
* Operation endpoints must be publicly reachable (the platform's dispatcher
|
|
234
|
+
* refuses private networks), so "the BFF is never public-facing" is achieved
|
|
235
|
+
* by AUTHENTICATED ingress, not network privacy: every route the platform
|
|
236
|
+
* calls verifies the platform-minted `X-Foir-Token` before doing anything. A
|
|
237
|
+
* route that skips verification is an open endpoint anyone can call
|
|
238
|
+
* directly, bypassing the operation's scopes, quotas, and preconditions.
|
|
239
|
+
*
|
|
240
|
+
* ```ts
|
|
241
|
+
* // Hono / Workers / Hydrogen — any fetch-API request:
|
|
242
|
+
* const { claims, subject, context } = await requireScopedToken(request, {
|
|
243
|
+
* expectedProjectId: env.FOIR_PROJECT_ID,
|
|
244
|
+
* });
|
|
245
|
+
* ```
|
|
246
|
+
*
|
|
247
|
+
* Throws {@link ScopedTokenError} with `status` 401 (missing/invalid token)
|
|
248
|
+
* or 403 (valid token for the wrong tenant/project) — map it to a response
|
|
249
|
+
* in one place:
|
|
250
|
+
*
|
|
251
|
+
* ```ts
|
|
252
|
+
* catch (err) {
|
|
253
|
+
* if (err instanceof ScopedTokenError) {
|
|
254
|
+
* return new Response(err.message, { status: err.status });
|
|
255
|
+
* }
|
|
256
|
+
* throw err;
|
|
257
|
+
* }
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Verification failure with the HTTP status the route should answer with:
|
|
263
|
+
* 401 for a missing or invalid token, 403 for a valid token bound to the
|
|
264
|
+
* wrong tenant or project.
|
|
265
|
+
*/
|
|
266
|
+
declare class ScopedTokenError extends Error {
|
|
267
|
+
readonly status: 401 | 403;
|
|
268
|
+
constructor(message: string, status: 401 | 403);
|
|
269
|
+
}
|
|
270
|
+
/** The minimal request shape needed — satisfied by any fetch-API Request. */
|
|
271
|
+
interface HeaderCarrier {
|
|
272
|
+
headers: {
|
|
273
|
+
get(name: string): string | null;
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
interface RequireScopedTokenOptions extends VerifyScopedTokenOptions {
|
|
277
|
+
/**
|
|
278
|
+
* JWKS endpoint to verify against. Defaults to the production platform
|
|
279
|
+
* (`https://api.foir.dev/.well-known/jwks.json`); point it at your dev
|
|
280
|
+
* host's `/.well-known/jwks.json` for non-production deployments.
|
|
281
|
+
*/
|
|
282
|
+
jwksUrl?: string;
|
|
283
|
+
/**
|
|
284
|
+
* When set, a verified token whose subject names a different project is
|
|
285
|
+
* rejected with 403. Set this from your deploy's own config — it pins the
|
|
286
|
+
* route to its project even if a token minted for another project leaks.
|
|
287
|
+
*/
|
|
288
|
+
expectedProjectId?: string;
|
|
289
|
+
/** Tenant analogue of {@link expectedProjectId}. */
|
|
290
|
+
expectedTenantId?: string;
|
|
291
|
+
}
|
|
292
|
+
interface ScopedTokenIngress {
|
|
293
|
+
claims: ScopedTokenClaims;
|
|
294
|
+
subject: ScopedTokenSubject;
|
|
295
|
+
/** Parsed `X-Foir-Context` correlation metadata (empty when absent). */
|
|
296
|
+
context: Record<string, string>;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Verify the platform-minted scoped token on an inbound dispatch request.
|
|
300
|
+
* Returns the decoded claims, parsed subject, and dispatch context; throws
|
|
301
|
+
* {@link ScopedTokenError} otherwise. See the module doc for the catch
|
|
302
|
+
* pattern.
|
|
303
|
+
*/
|
|
304
|
+
declare function requireScopedToken(request: HeaderCarrier, options?: RequireScopedTokenOptions): Promise<ScopedTokenIngress>;
|
|
305
|
+
|
|
229
306
|
/**
|
|
230
307
|
* Type definitions for platform operations.
|
|
231
308
|
*
|
|
@@ -454,4 +531,4 @@ declare function createCallbackClient<TResult = unknown>(callback: OperationCall
|
|
|
454
531
|
fetch?: typeof fetch;
|
|
455
532
|
}): CallbackClient<TResult>;
|
|
456
533
|
|
|
457
|
-
export { type CallbackClient, type CallbackResult, type EditorPlacement, type GraphQLResponse, type OperationCallback, type OperationContext, type OperationPayload, type OperationRecordContext, type OperationResult, type OperationTrigger, type ScopedTokenClaims, type ScopedTokenSubject, type ServerClient, ServerClientError, type ServerClientOptions, type VerifyScopedTokenOptions, createCallbackClient, createServerClient, parseContextHeader, parseSubject, recordsReadModels, recordsWriteModels, verifyScopedToken, verifyWebhookSignature };
|
|
534
|
+
export { type CallbackClient, type CallbackResult, type EditorPlacement, type GraphQLResponse, type OperationCallback, type OperationContext, type OperationPayload, type OperationRecordContext, type OperationResult, type OperationTrigger, type RequireScopedTokenOptions, type ScopedTokenClaims, ScopedTokenError, type ScopedTokenIngress, type ScopedTokenSubject, type ServerClient, ServerClientError, type ServerClientOptions, type VerifyScopedTokenOptions, createCallbackClient, createServerClient, parseContextHeader, parseSubject, recordsReadModels, recordsWriteModels, requireScopedToken, verifyScopedToken, verifyWebhookSignature };
|