@geekapps/auth-nextjs 0.16.1 → 0.18.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/dist/connections.d.ts +121 -67
- package/dist/connections.js +134 -957
- package/dist/connections.js.map +1 -1
- package/package.json +10 -3
package/dist/connections.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/connections.ts
|
|
2
2
|
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
3
|
-
import { buildModel, fetchRemoteCatalog, renderAugmentation, toRegistry } from "@geekapps/auth-cli/lib";
|
|
4
3
|
import { cookies as cookies2 } from "next/headers";
|
|
5
4
|
import { redirect as redirect2 } from "next/navigation";
|
|
6
5
|
|
|
@@ -10,794 +9,7 @@ import { cookies, headers } from "next/headers";
|
|
|
10
9
|
import { redirect } from "next/navigation";
|
|
11
10
|
import { NextResponse } from "next/server";
|
|
12
11
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
13
|
-
var GeekappsSessionExpiredError = class extends Error {
|
|
14
|
-
code = "session_expired";
|
|
15
|
-
constructor(message = "A sess\xE3o expirou ou foi revogada. Entre novamente.") {
|
|
16
|
-
super(message);
|
|
17
|
-
this.name = "GeekappsSessionExpiredError";
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
12
|
var GEEKAPPS_AUTH_SERVER_URL = "https://accounts-api.geekapps.com.br";
|
|
21
|
-
var MAX_ACCOUNTS = 8;
|
|
22
|
-
function normalizeBaseUrl(url) {
|
|
23
|
-
return url.replace(/\/$/, "");
|
|
24
|
-
}
|
|
25
|
-
function normalizeBasePath(path = "/api/auth") {
|
|
26
|
-
if (!path.startsWith("/")) return `/${path}`;
|
|
27
|
-
return path.replace(/\/$/, "");
|
|
28
|
-
}
|
|
29
|
-
function createUrl(baseUrl, path) {
|
|
30
|
-
if (/^https?:\/\//.test(path)) return path;
|
|
31
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
32
|
-
return `${normalizeBaseUrl(baseUrl)}${normalizedPath}`;
|
|
33
|
-
}
|
|
34
|
-
function env(name) {
|
|
35
|
-
const value = process.env[name];
|
|
36
|
-
return value && value.trim().length > 0 ? value : void 0;
|
|
37
|
-
}
|
|
38
|
-
function resolveIssuer(input) {
|
|
39
|
-
return normalizeBaseUrl(
|
|
40
|
-
input.issuer ?? env("GEEKAPPS_AUTH_SERVER_ISSUER") ?? env("GEEKAPPS_AUTH_SERVER_URL") ?? // Deprecated: kept for backwards compatibility during the auth-server rename.
|
|
41
|
-
env("GEEKAPPS_AUTH_ISSUER") ?? env("GEEKAPPS_AUTH_API_URL") ?? GEEKAPPS_AUTH_SERVER_URL
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
function resolveClientId(input) {
|
|
45
|
-
return input.clientId ?? env("GEEKAPPS_AUTH_CLIENT_ID") ?? "";
|
|
46
|
-
}
|
|
47
|
-
function resolveClientSecret(input) {
|
|
48
|
-
return input.clientSecret ?? env("GEEKAPPS_AUTH_CLIENT_SECRET");
|
|
49
|
-
}
|
|
50
|
-
function applyClientCredentials(url, config) {
|
|
51
|
-
if (!config.clientId || !config.clientSecret) return;
|
|
52
|
-
url.searchParams.set("client_id", config.clientId);
|
|
53
|
-
url.searchParams.set("client_secret", config.clientSecret);
|
|
54
|
-
}
|
|
55
|
-
function resolveConfiguredRedirectUri(input) {
|
|
56
|
-
return input.redirectUri ?? env("GEEKAPPS_AUTH_REDIRECT_URI");
|
|
57
|
-
}
|
|
58
|
-
function hashClientId(clientId) {
|
|
59
|
-
let h = 5381;
|
|
60
|
-
for (let i = 0; i < clientId.length; i++) {
|
|
61
|
-
h = (h << 5) + h ^ clientId.charCodeAt(i) & 255;
|
|
62
|
-
h = h >>> 0;
|
|
63
|
-
}
|
|
64
|
-
return h.toString(16).padStart(8, "0");
|
|
65
|
-
}
|
|
66
|
-
function deriveCookiePrefix(clientId) {
|
|
67
|
-
if (!clientId) return "gk";
|
|
68
|
-
return `gk_${hashClientId(clientId)}`;
|
|
69
|
-
}
|
|
70
|
-
function requestOrigin(request) {
|
|
71
|
-
const configured = env("GEEKAPPS_APP_URL");
|
|
72
|
-
if (configured) return configured.replace(/\/$/, "");
|
|
73
|
-
if (request.nextUrl?.origin) return request.nextUrl.origin;
|
|
74
|
-
throw new Error(
|
|
75
|
-
"Cannot determine app origin. Set GEEKAPPS_APP_URL or ensure request.nextUrl is available."
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
function resolveRedirectUri(config, request) {
|
|
79
|
-
return config.redirectUri ?? `${requestOrigin(request)}${config.basePath}/callback`;
|
|
80
|
-
}
|
|
81
|
-
function tokenFromResponse(data) {
|
|
82
|
-
return {
|
|
83
|
-
accessToken: data.accessToken ?? data.access_token ?? data.tokens?.accessToken ?? data.tokens?.access_token ?? null,
|
|
84
|
-
refreshToken: data.refreshToken ?? data.refresh_token ?? data.tokens?.refreshToken ?? data.tokens?.refresh_token ?? null,
|
|
85
|
-
idToken: data.idToken ?? data.id_token ?? data.tokens?.idToken ?? data.tokens?.id_token ?? null,
|
|
86
|
-
sessionId: data.sessionId ?? data.session_id ?? data.tokens?.sessionId ?? data.tokens?.session_id ?? null
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
function decodeJwtPayload(token) {
|
|
90
|
-
try {
|
|
91
|
-
const parts = token.split(".");
|
|
92
|
-
if (parts.length < 2) return null;
|
|
93
|
-
const payload = parts[1];
|
|
94
|
-
if (!payload) return null;
|
|
95
|
-
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
96
|
-
} catch {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
function parseAccountFromJwt(tokens) {
|
|
101
|
-
if (!tokens.accessToken) return null;
|
|
102
|
-
const jwtToParse = tokens.idToken ?? tokens.accessToken;
|
|
103
|
-
const payload = decodeJwtPayload(jwtToParse);
|
|
104
|
-
if (!payload) return null;
|
|
105
|
-
const sub = typeof payload.sub === "string" ? payload.sub : null;
|
|
106
|
-
const id = sub ?? tokens.sessionId;
|
|
107
|
-
if (!id) return null;
|
|
108
|
-
return {
|
|
109
|
-
id,
|
|
110
|
-
accessToken: tokens.accessToken,
|
|
111
|
-
refreshToken: tokens.refreshToken,
|
|
112
|
-
idToken: tokens.idToken ?? null,
|
|
113
|
-
sessionId: tokens.sessionId,
|
|
114
|
-
user: {
|
|
115
|
-
sub: id,
|
|
116
|
-
name: typeof payload.name === "string" ? payload.name : null,
|
|
117
|
-
email: typeof payload.email === "string" ? payload.email : null,
|
|
118
|
-
picture: typeof payload.picture === "string" ? payload.picture : null,
|
|
119
|
-
phone: typeof payload.phone_number === "string" ? payload.phone_number : null
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
function getCookieNames(prefix) {
|
|
124
|
-
return {
|
|
125
|
-
active: `${prefix}_active`,
|
|
126
|
-
count: `${prefix}_count`,
|
|
127
|
-
activeOrganizationId: `${prefix}_ao`,
|
|
128
|
-
tokenPub: `${prefix}_tp`,
|
|
129
|
-
pkceVerifier: `${prefix}_pkce`,
|
|
130
|
-
slotMeta: (n) => `${prefix}_m${n}`,
|
|
131
|
-
slotAt: (n) => `${prefix}_at${n}`,
|
|
132
|
-
slotRt: (n) => `${prefix}_rt${n}`
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function generateCodeVerifier() {
|
|
136
|
-
return randomBytes(64).toString("base64url");
|
|
137
|
-
}
|
|
138
|
-
function createCodeChallenge(codeVerifier) {
|
|
139
|
-
return createHash("sha256").update(codeVerifier).digest("base64url");
|
|
140
|
-
}
|
|
141
|
-
function encodeMeta(account) {
|
|
142
|
-
const m = { id: account.id };
|
|
143
|
-
if (account.sessionId) m.sid = account.sessionId;
|
|
144
|
-
if (account.user.name) m.n = account.user.name;
|
|
145
|
-
if (account.user.email) m.e = account.user.email;
|
|
146
|
-
if (account.user.picture) m.p = account.user.picture;
|
|
147
|
-
return Buffer.from(JSON.stringify(m), "utf8").toString("base64url");
|
|
148
|
-
}
|
|
149
|
-
function decodeMeta(value) {
|
|
150
|
-
try {
|
|
151
|
-
const m = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
152
|
-
return m.id ? m : null;
|
|
153
|
-
} catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
function getCookieOptions(config) {
|
|
158
|
-
return {
|
|
159
|
-
httpOnly: true,
|
|
160
|
-
secure: config.secureCookies ?? process.env.NODE_ENV === "production",
|
|
161
|
-
sameSite: "lax",
|
|
162
|
-
path: "/"
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
async function readBody(request) {
|
|
166
|
-
const text = await request.text();
|
|
167
|
-
if (!text) return void 0;
|
|
168
|
-
try {
|
|
169
|
-
return JSON.parse(text);
|
|
170
|
-
} catch {
|
|
171
|
-
return text;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
function appendSearchParams(url, request) {
|
|
175
|
-
const target = new URL(url);
|
|
176
|
-
request.nextUrl.searchParams.forEach((value, key) => {
|
|
177
|
-
target.searchParams.set(key, value);
|
|
178
|
-
});
|
|
179
|
-
return target.toString();
|
|
180
|
-
}
|
|
181
|
-
function routeFromRequest(request, basePath) {
|
|
182
|
-
const pathname = request.nextUrl.pathname;
|
|
183
|
-
if (!pathname.startsWith(basePath)) return pathname;
|
|
184
|
-
const route = pathname.slice(basePath.length);
|
|
185
|
-
return route || "/";
|
|
186
|
-
}
|
|
187
|
-
function isJsonResponse(response) {
|
|
188
|
-
return response.headers.get("content-type")?.includes("application/json");
|
|
189
|
-
}
|
|
190
|
-
async function responseFromApi(response) {
|
|
191
|
-
if (response.status === 204) return new NextResponse(null, { status: 204 });
|
|
192
|
-
const body = isJsonResponse(response) ? await response.json().catch(() => null) : await response.text();
|
|
193
|
-
return NextResponse.json(body, { status: response.status });
|
|
194
|
-
}
|
|
195
|
-
var _remoteJWKSCache = /* @__PURE__ */ new Map();
|
|
196
|
-
function getRemoteJWKS(issuer) {
|
|
197
|
-
if (!_remoteJWKSCache.has(issuer)) {
|
|
198
|
-
_remoteJWKSCache.set(issuer, createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`)));
|
|
199
|
-
}
|
|
200
|
-
return _remoteJWKSCache.get(issuer);
|
|
201
|
-
}
|
|
202
|
-
async function verifyToken(token, issuer) {
|
|
203
|
-
try {
|
|
204
|
-
const { payload } = await jwtVerify(token, getRemoteJWKS(issuer), {
|
|
205
|
-
algorithms: ["RS256"],
|
|
206
|
-
issuer
|
|
207
|
-
});
|
|
208
|
-
return payload;
|
|
209
|
-
} catch {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
function configureGeekappsAuth(input = {}) {
|
|
214
|
-
const clientId = resolveClientId(input);
|
|
215
|
-
const config = {
|
|
216
|
-
...input,
|
|
217
|
-
issuer: resolveIssuer(input),
|
|
218
|
-
clientId,
|
|
219
|
-
clientSecret: resolveClientSecret(input),
|
|
220
|
-
redirectUri: resolveConfiguredRedirectUri(input),
|
|
221
|
-
afterSignInRedirectTo: input.afterSignInRedirectTo ?? input.redirectTo ?? "/",
|
|
222
|
-
basePath: normalizeBasePath(input.basePath),
|
|
223
|
-
cookiePrefix: deriveCookiePrefix(clientId)
|
|
224
|
-
};
|
|
225
|
-
const cn = getCookieNames(config.cookiePrefix);
|
|
226
|
-
async function getCount() {
|
|
227
|
-
const cookieStore = await cookies();
|
|
228
|
-
const raw = cookieStore.get(cn.count)?.value;
|
|
229
|
-
const n = raw ? Number.parseInt(raw, 10) : 0;
|
|
230
|
-
return Number.isNaN(n) ? 0 : n;
|
|
231
|
-
}
|
|
232
|
-
async function getActiveIndex() {
|
|
233
|
-
const cookieStore = await cookies();
|
|
234
|
-
const raw = cookieStore.get(cn.active)?.value;
|
|
235
|
-
if (!raw) return 0;
|
|
236
|
-
const n = Number.parseInt(raw, 10);
|
|
237
|
-
return Number.isNaN(n) ? 0 : n;
|
|
238
|
-
}
|
|
239
|
-
function readSlot(cookieStore, n) {
|
|
240
|
-
const at = cookieStore.get(cn.slotAt(n))?.value;
|
|
241
|
-
if (!at) return null;
|
|
242
|
-
const meta = decodeMeta(cookieStore.get(cn.slotMeta(n))?.value ?? "");
|
|
243
|
-
if (!meta) return null;
|
|
244
|
-
return {
|
|
245
|
-
id: meta.id,
|
|
246
|
-
accessToken: at,
|
|
247
|
-
refreshToken: cookieStore.get(cn.slotRt(n))?.value ?? null,
|
|
248
|
-
idToken: null,
|
|
249
|
-
// not stored in cookies; claims are in meta
|
|
250
|
-
sessionId: meta.sid ?? null,
|
|
251
|
-
user: {
|
|
252
|
-
sub: meta.id,
|
|
253
|
-
name: meta.n ?? null,
|
|
254
|
-
email: meta.e ?? null,
|
|
255
|
-
picture: meta.p ?? null
|
|
256
|
-
}
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
function writeSlot(cookieStore, n, account) {
|
|
260
|
-
const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };
|
|
261
|
-
cookieStore.set(cn.slotMeta(n), encodeMeta(account), opts);
|
|
262
|
-
cookieStore.set(cn.slotAt(n), account.accessToken, opts);
|
|
263
|
-
if (account.refreshToken) cookieStore.set(cn.slotRt(n), account.refreshToken, opts);
|
|
264
|
-
}
|
|
265
|
-
function clearSlot(cookieStore, n) {
|
|
266
|
-
const clear = { ...getCookieOptions(config), maxAge: 0 };
|
|
267
|
-
cookieStore.set(cn.slotMeta(n), "", clear);
|
|
268
|
-
cookieStore.set(cn.slotAt(n), "", clear);
|
|
269
|
-
cookieStore.set(cn.slotRt(n), "", clear);
|
|
270
|
-
}
|
|
271
|
-
async function getAccountsList() {
|
|
272
|
-
const [cookieStore, count] = await Promise.all([cookies(), getCount()]);
|
|
273
|
-
const accounts = [];
|
|
274
|
-
for (let i = 1; i <= count; i++) {
|
|
275
|
-
const slot = readSlot(cookieStore, i);
|
|
276
|
-
if (slot) accounts.push(slot);
|
|
277
|
-
}
|
|
278
|
-
return accounts;
|
|
279
|
-
}
|
|
280
|
-
async function getActiveAccount() {
|
|
281
|
-
const [cookieStore, activeIdx, count] = await Promise.all([cookies(), getActiveIndex(), getCount()]);
|
|
282
|
-
if (count === 0) return null;
|
|
283
|
-
if (activeIdx >= 1 && activeIdx <= count) {
|
|
284
|
-
const slot = readSlot(cookieStore, activeIdx);
|
|
285
|
-
if (slot) return slot;
|
|
286
|
-
}
|
|
287
|
-
return readSlot(cookieStore, 1);
|
|
288
|
-
}
|
|
289
|
-
async function getAccessToken() {
|
|
290
|
-
try {
|
|
291
|
-
const hdrs = await headers();
|
|
292
|
-
const auth = hdrs.get("authorization");
|
|
293
|
-
if (auth?.startsWith("Bearer ")) return auth.slice(7);
|
|
294
|
-
} catch {
|
|
295
|
-
}
|
|
296
|
-
const active = await getActiveAccount();
|
|
297
|
-
return active?.accessToken ?? null;
|
|
298
|
-
}
|
|
299
|
-
async function getRefreshToken() {
|
|
300
|
-
const active = await getActiveAccount();
|
|
301
|
-
return active?.refreshToken ?? null;
|
|
302
|
-
}
|
|
303
|
-
async function getSession() {
|
|
304
|
-
const active = await getActiveAccount();
|
|
305
|
-
if (!active) return null;
|
|
306
|
-
return {
|
|
307
|
-
accessToken: active.accessToken,
|
|
308
|
-
refreshToken: active.refreshToken,
|
|
309
|
-
idToken: active.idToken,
|
|
310
|
-
sessionId: active.sessionId
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
async function getUser() {
|
|
314
|
-
const active = await getActiveAccount();
|
|
315
|
-
return active?.user ?? null;
|
|
316
|
-
}
|
|
317
|
-
async function getUserById(userId, applicationId) {
|
|
318
|
-
const qs = new URLSearchParams({ ...applicationId ? { applicationId } : {} });
|
|
319
|
-
try {
|
|
320
|
-
return await apiRequest(`/users/${userId}?${qs}`);
|
|
321
|
-
} catch {
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
async function getOrganizations() {
|
|
326
|
-
const cookieStore = await cookies();
|
|
327
|
-
const currentOrgId = cookieStore.get(cn.activeOrganizationId)?.value ?? null;
|
|
328
|
-
try {
|
|
329
|
-
const data = await apiRequest("/organizations");
|
|
330
|
-
const organizations = Array.isArray(data) ? data : data.organizations;
|
|
331
|
-
return { organizations, currentOrgId };
|
|
332
|
-
} catch {
|
|
333
|
-
return { organizations: [], currentOrgId };
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
function clearPublicTokenCookie(cookieStore) {
|
|
337
|
-
if (cookieStore.get(cn.tokenPub)?.value) {
|
|
338
|
-
cookieStore.set(cn.tokenPub, "", { httpOnly: false, maxAge: 0, path: "/" });
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
async function saveAccountToList(tokens) {
|
|
342
|
-
const account = parseAccountFromJwt(tokens);
|
|
343
|
-
if (!account) return;
|
|
344
|
-
const cookieStore = await cookies();
|
|
345
|
-
const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };
|
|
346
|
-
const count = await getCount();
|
|
347
|
-
let targetSlot = 0;
|
|
348
|
-
for (let i = 1; i <= count; i++) {
|
|
349
|
-
const existing = readSlot(cookieStore, i);
|
|
350
|
-
if (existing?.id === account.id) {
|
|
351
|
-
targetSlot = i;
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if (targetSlot === 0) {
|
|
356
|
-
if (count >= MAX_ACCOUNTS) return;
|
|
357
|
-
targetSlot = count + 1;
|
|
358
|
-
cookieStore.set(cn.count, String(targetSlot), opts);
|
|
359
|
-
}
|
|
360
|
-
writeSlot(cookieStore, targetSlot, account);
|
|
361
|
-
cookieStore.set(cn.active, String(targetSlot), opts);
|
|
362
|
-
clearPublicTokenCookie(cookieStore);
|
|
363
|
-
}
|
|
364
|
-
const setSessionCookies = saveAccountToList;
|
|
365
|
-
async function switchAccount(accountId) {
|
|
366
|
-
const [cookieStore, count] = await Promise.all([cookies(), getCount()]);
|
|
367
|
-
const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };
|
|
368
|
-
for (let i = 1; i <= count; i++) {
|
|
369
|
-
const slot = readSlot(cookieStore, i);
|
|
370
|
-
if (slot && (slot.id === accountId || slot.sessionId === accountId)) {
|
|
371
|
-
cookieStore.set(cn.active, String(i), opts);
|
|
372
|
-
clearPublicTokenCookie(cookieStore);
|
|
373
|
-
return true;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
|
-
async function removeActiveAccount() {
|
|
379
|
-
const [cookieStore, activeIdx, count] = await Promise.all([cookies(), getActiveIndex(), getCount()]);
|
|
380
|
-
const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };
|
|
381
|
-
const clear = { ...getCookieOptions(config), maxAge: 0 };
|
|
382
|
-
if (count === 0) return;
|
|
383
|
-
const remaining = [];
|
|
384
|
-
for (let i = 1; i <= count; i++) {
|
|
385
|
-
if (i === activeIdx) continue;
|
|
386
|
-
const slot = readSlot(cookieStore, i);
|
|
387
|
-
if (slot) remaining.push(slot);
|
|
388
|
-
}
|
|
389
|
-
for (let i = 1; i <= count; i++) {
|
|
390
|
-
clearSlot(cookieStore, i);
|
|
391
|
-
}
|
|
392
|
-
if (remaining.length === 0) {
|
|
393
|
-
cookieStore.set(cn.count, "0", clear);
|
|
394
|
-
cookieStore.set(cn.active, "0", clear);
|
|
395
|
-
await clearPublicTokenCookie(cookieStore);
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
for (let i = 0; i < remaining.length; i++) {
|
|
399
|
-
await writeSlot(cookieStore, i + 1, remaining[i]);
|
|
400
|
-
}
|
|
401
|
-
cookieStore.set(cn.count, String(remaining.length), opts);
|
|
402
|
-
cookieStore.set(cn.active, "1", opts);
|
|
403
|
-
clearPublicTokenCookie(cookieStore);
|
|
404
|
-
}
|
|
405
|
-
async function clearAllSessionCookies() {
|
|
406
|
-
const [cookieStore, count] = await Promise.all([cookies(), getCount()]);
|
|
407
|
-
const clear = { ...getCookieOptions(config), maxAge: 0 };
|
|
408
|
-
for (let i = 1; i <= count; i++) {
|
|
409
|
-
clearSlot(cookieStore, i);
|
|
410
|
-
}
|
|
411
|
-
cookieStore.set(cn.count, "0", clear);
|
|
412
|
-
cookieStore.set(cn.active, "0", clear);
|
|
413
|
-
clearPublicTokenCookie(cookieStore);
|
|
414
|
-
}
|
|
415
|
-
async function tryRefreshTokens() {
|
|
416
|
-
const refreshToken = await getRefreshToken();
|
|
417
|
-
if (!refreshToken || !config.clientId) return null;
|
|
418
|
-
const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret ?? ""}`).toString("base64");
|
|
419
|
-
const response = await fetch(createUrl(config.issuer, "/auth/token"), {
|
|
420
|
-
method: "POST",
|
|
421
|
-
headers: {
|
|
422
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
423
|
-
authorization: `Basic ${basicAuth}`
|
|
424
|
-
},
|
|
425
|
-
body: new URLSearchParams({
|
|
426
|
-
grant_type: "refresh_token",
|
|
427
|
-
refresh_token: refreshToken
|
|
428
|
-
}),
|
|
429
|
-
cache: "no-store"
|
|
430
|
-
});
|
|
431
|
-
if (!response.ok) return null;
|
|
432
|
-
const data = await response.json().catch(() => null);
|
|
433
|
-
if (!data) return null;
|
|
434
|
-
const tokens = tokenFromResponse(data);
|
|
435
|
-
if (!tokens.accessToken) return null;
|
|
436
|
-
await saveAccountToList(tokens);
|
|
437
|
-
return tokens.accessToken;
|
|
438
|
-
}
|
|
439
|
-
async function apiRequest(path, init = {}) {
|
|
440
|
-
const accessToken = await getAccessToken();
|
|
441
|
-
const requestHeaders = new Headers(init.headers);
|
|
442
|
-
if (accessToken) requestHeaders.set("authorization", `Bearer ${accessToken}`);
|
|
443
|
-
if (init.body !== void 0 && !requestHeaders.has("content-type")) {
|
|
444
|
-
requestHeaders.set("content-type", "application/json");
|
|
445
|
-
}
|
|
446
|
-
const url = new URL(createUrl(config.issuer, path));
|
|
447
|
-
applyClientCredentials(url, config);
|
|
448
|
-
const response = await fetch(url, {
|
|
449
|
-
...init,
|
|
450
|
-
headers: requestHeaders,
|
|
451
|
-
cache: "no-store"
|
|
452
|
-
});
|
|
453
|
-
if (response.status === 401) {
|
|
454
|
-
const newAccessToken = await tryRefreshTokens();
|
|
455
|
-
if (!newAccessToken) {
|
|
456
|
-
await clearAllSessionCookies().catch(() => {
|
|
457
|
-
});
|
|
458
|
-
throw new GeekappsSessionExpiredError();
|
|
459
|
-
}
|
|
460
|
-
if (newAccessToken) {
|
|
461
|
-
const retryHeaders = new Headers(init.headers);
|
|
462
|
-
retryHeaders.set("authorization", `Bearer ${newAccessToken}`);
|
|
463
|
-
if (init.body !== void 0 && !retryHeaders.has("content-type")) {
|
|
464
|
-
retryHeaders.set("content-type", "application/json");
|
|
465
|
-
}
|
|
466
|
-
const retryUrl = new URL(createUrl(config.issuer, path));
|
|
467
|
-
applyClientCredentials(retryUrl, config);
|
|
468
|
-
const retryResponse = await fetch(retryUrl, {
|
|
469
|
-
...init,
|
|
470
|
-
headers: retryHeaders,
|
|
471
|
-
cache: "no-store"
|
|
472
|
-
});
|
|
473
|
-
if (!retryResponse.ok) {
|
|
474
|
-
const error = await retryResponse.json().catch(() => null);
|
|
475
|
-
throw new Error(error?.error_description ?? error?.error ?? error?.message ?? `Auth API error ${retryResponse.status}`);
|
|
476
|
-
}
|
|
477
|
-
if (retryResponse.status === 204) return null;
|
|
478
|
-
return await retryResponse.json();
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
if (!response.ok) {
|
|
482
|
-
const error = await response.json().catch(() => null);
|
|
483
|
-
throw new Error(error?.error_description ?? error?.error ?? error?.message ?? `Auth API error ${response.status}`);
|
|
484
|
-
}
|
|
485
|
-
if (response.status === 204) return null;
|
|
486
|
-
return await response.json();
|
|
487
|
-
}
|
|
488
|
-
function createApiClient() {
|
|
489
|
-
return {
|
|
490
|
-
get: (path) => apiRequest(path),
|
|
491
|
-
post: (path, body) => apiRequest(path, { method: "POST", body: body === void 0 ? void 0 : JSON.stringify(body) }),
|
|
492
|
-
patch: (path, body) => apiRequest(path, { method: "PATCH", body: body === void 0 ? void 0 : JSON.stringify(body) }),
|
|
493
|
-
put: (path, body) => apiRequest(path, { method: "PUT", body: body === void 0 ? void 0 : JSON.stringify(body) }),
|
|
494
|
-
delete: (path) => apiRequest(path, { method: "DELETE" })
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
async function requireUser(options = {}) {
|
|
498
|
-
const accessToken = await getAccessToken();
|
|
499
|
-
if (!accessToken) {
|
|
500
|
-
redirect(options.redirectTo ?? "/");
|
|
501
|
-
}
|
|
502
|
-
return accessToken;
|
|
503
|
-
}
|
|
504
|
-
function escapeHtml(str) {
|
|
505
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
506
|
-
}
|
|
507
|
-
function sanitizeRedirectTo(value) {
|
|
508
|
-
const raw = value ?? "/";
|
|
509
|
-
if (raw.startsWith(config.basePath)) return "/";
|
|
510
|
-
if (raw.startsWith("/") && !raw.startsWith("//")) return raw;
|
|
511
|
-
return "/";
|
|
512
|
-
}
|
|
513
|
-
async function createAuthorizeRedirect(request, mode) {
|
|
514
|
-
const redirectTo = sanitizeRedirectTo(request.nextUrl.searchParams.get("redirectTo") ?? config.afterSignInRedirectTo);
|
|
515
|
-
const flowId = request.nextUrl.searchParams.get("flow_id");
|
|
516
|
-
const url = new URL(createUrl(config.issuer, mode === "google" ? "/auth/google" : "/authorize"));
|
|
517
|
-
if (!config.clientId) {
|
|
518
|
-
return NextResponse.json(
|
|
519
|
-
{ error: "missing_client_id", message: "Missing GEEKAPPS_AUTH_CLIENT_ID or clientId." },
|
|
520
|
-
{ status: 500 }
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
url.searchParams.set("client_id", config.clientId);
|
|
524
|
-
url.searchParams.set("redirect_uri", resolveRedirectUri(config, request));
|
|
525
|
-
url.searchParams.set("response_type", "code");
|
|
526
|
-
const scopes = config.scopes ?? ["openid", "profile", "email"];
|
|
527
|
-
if (!scopes.includes("offline_access")) scopes.push("offline_access");
|
|
528
|
-
url.searchParams.set("scope", scopes.join(" "));
|
|
529
|
-
url.searchParams.set("state", Buffer.from(JSON.stringify({ redirectTo, mode })).toString("base64url"));
|
|
530
|
-
if (mode === "register") url.searchParams.set("screen_hint", "signup");
|
|
531
|
-
if (mode === "add-account") {
|
|
532
|
-
url.searchParams.set("prompt", "login");
|
|
533
|
-
} else {
|
|
534
|
-
const prompt = request.nextUrl.searchParams.get("prompt");
|
|
535
|
-
const silentLogin = request.nextUrl.searchParams.get("silentLogin");
|
|
536
|
-
const resolvedPrompt = prompt ?? (silentLogin === "false" ? "login" : null);
|
|
537
|
-
if (resolvedPrompt) url.searchParams.set("prompt", resolvedPrompt);
|
|
538
|
-
}
|
|
539
|
-
if (flowId) url.searchParams.set("flow_id", flowId);
|
|
540
|
-
const codeVerifier = generateCodeVerifier();
|
|
541
|
-
url.searchParams.set("code_challenge", createCodeChallenge(codeVerifier));
|
|
542
|
-
url.searchParams.set("code_challenge_method", "S256");
|
|
543
|
-
const response = NextResponse.redirect(url);
|
|
544
|
-
const cn2 = getCookieNames(config.cookiePrefix);
|
|
545
|
-
response.cookies.set(cn2.pkceVerifier, codeVerifier, {
|
|
546
|
-
...getCookieOptions(config),
|
|
547
|
-
maxAge: 60 * 10
|
|
548
|
-
});
|
|
549
|
-
return response;
|
|
550
|
-
}
|
|
551
|
-
async function exchangeCode(request) {
|
|
552
|
-
const code = request.nextUrl.searchParams.get("code");
|
|
553
|
-
const state = request.nextUrl.searchParams.get("state");
|
|
554
|
-
const origin = requestOrigin(request);
|
|
555
|
-
if (!code) {
|
|
556
|
-
const err = request.nextUrl.searchParams.get("error") ?? "missing_code";
|
|
557
|
-
const desc = request.nextUrl.searchParams.get("error_description") ?? request.nextUrl.searchParams.get("error_message") ?? "No authorization code received";
|
|
558
|
-
return NextResponse.redirect(new URL(`${config.basePath}/callback?error=${encodeURIComponent(err)}&error_description=${encodeURIComponent(desc)}`, origin));
|
|
559
|
-
}
|
|
560
|
-
if (!config.clientId) {
|
|
561
|
-
return NextResponse.redirect(new URL(`${config.basePath}/callback?error=missing_client_id&error_description=Auth+client+is+not+configured`, origin));
|
|
562
|
-
}
|
|
563
|
-
const basicAuth = Buffer.from(
|
|
564
|
-
`${config.clientId}:${config.clientSecret ?? ""}`
|
|
565
|
-
).toString("base64");
|
|
566
|
-
const cn2 = getCookieNames(config.cookiePrefix);
|
|
567
|
-
const cookieStore = await cookies();
|
|
568
|
-
const codeVerifier = cookieStore.get(cn2.pkceVerifier)?.value;
|
|
569
|
-
cookieStore.set(cn2.pkceVerifier, "", { ...getCookieOptions(config), maxAge: 0 });
|
|
570
|
-
const tokenBody = new URLSearchParams({
|
|
571
|
-
grant_type: "authorization_code",
|
|
572
|
-
code,
|
|
573
|
-
redirect_uri: resolveRedirectUri(config, request)
|
|
574
|
-
});
|
|
575
|
-
if (codeVerifier) tokenBody.set("code_verifier", codeVerifier);
|
|
576
|
-
const response = await fetch(createUrl(config.issuer, "/auth/token"), {
|
|
577
|
-
method: "POST",
|
|
578
|
-
headers: {
|
|
579
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
580
|
-
authorization: `Basic ${basicAuth}`
|
|
581
|
-
},
|
|
582
|
-
body: tokenBody,
|
|
583
|
-
cache: "no-store"
|
|
584
|
-
});
|
|
585
|
-
const data = await response.json().catch(() => ({}));
|
|
586
|
-
if (!response.ok) {
|
|
587
|
-
const error = encodeURIComponent(data.error ?? "token_exchange_failed");
|
|
588
|
-
const desc = encodeURIComponent(data.error_description ?? `Token exchange failed with status ${response.status}`);
|
|
589
|
-
return NextResponse.redirect(new URL(`${config.basePath}/callback?error=${error}&error_description=${desc}`, origin));
|
|
590
|
-
}
|
|
591
|
-
await saveAccountToList(tokenFromResponse(data));
|
|
592
|
-
let redirectTo = sanitizeRedirectTo(config.afterSignInRedirectTo);
|
|
593
|
-
if (state) {
|
|
594
|
-
try {
|
|
595
|
-
const parsed = JSON.parse(Buffer.from(state, "base64url").toString("utf8"));
|
|
596
|
-
redirectTo = sanitizeRedirectTo(parsed.redirectTo);
|
|
597
|
-
} catch {
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
return NextResponse.redirect(new URL(redirectTo, origin));
|
|
601
|
-
}
|
|
602
|
-
function mapLocalPathToApiPath(route) {
|
|
603
|
-
if (route === "/me") return "/auth/me";
|
|
604
|
-
if (route === "/accounts/switch") return "/auth/accounts/switch";
|
|
605
|
-
if (route === "/organizations/active") return "/auth/organizations/active";
|
|
606
|
-
if (route === "/permissions/check") return "/auth/permissions/check";
|
|
607
|
-
if (route === "/login/password") return "/auth/login/password";
|
|
608
|
-
if (route === "/register/password") return "/auth/register/password";
|
|
609
|
-
if (route === "/passwordless/link") return "/auth/passwordless/link";
|
|
610
|
-
return route;
|
|
611
|
-
}
|
|
612
|
-
async function handleAccountsLocal() {
|
|
613
|
-
const [accounts, activeIdx] = await Promise.all([getAccountsList(), getActiveIndex()]);
|
|
614
|
-
return NextResponse.json({
|
|
615
|
-
accounts: accounts.map((a, i) => ({
|
|
616
|
-
id: a.id,
|
|
617
|
-
sessionId: a.sessionId ?? a.id,
|
|
618
|
-
isActive: i + 1 === activeIdx,
|
|
619
|
-
user: {
|
|
620
|
-
id: a.user.sub,
|
|
621
|
-
name: a.user.name ?? null,
|
|
622
|
-
email: a.user.email ?? null,
|
|
623
|
-
imageUrl: a.user.picture ?? null,
|
|
624
|
-
phone: a.user.phone ?? null
|
|
625
|
-
}
|
|
626
|
-
}))
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
async function handleOrganizationsLocal() {
|
|
630
|
-
const cookieStore = await cookies();
|
|
631
|
-
const activeOrgId = cookieStore.get(cn.activeOrganizationId)?.value ?? null;
|
|
632
|
-
try {
|
|
633
|
-
const data = await apiRequest("/organizations");
|
|
634
|
-
const organizations = Array.isArray(data) ? data : data.organizations;
|
|
635
|
-
return NextResponse.json({ organizations, currentOrgId: activeOrgId });
|
|
636
|
-
} catch {
|
|
637
|
-
return NextResponse.json({ organizations: [], currentOrgId: activeOrgId });
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
async function handleSwitchAccountLocal(request) {
|
|
641
|
-
const body = await readBody(request);
|
|
642
|
-
const targetId = body && typeof body === "object" ? String(
|
|
643
|
-
body.accountId ?? body.sessionId ?? ""
|
|
644
|
-
) : "";
|
|
645
|
-
if (!targetId) {
|
|
646
|
-
return NextResponse.json({ error: "accountId required" }, { status: 400 });
|
|
647
|
-
}
|
|
648
|
-
const switched = await switchAccount(targetId);
|
|
649
|
-
if (!switched) {
|
|
650
|
-
return NextResponse.json({ error: "Account not found" }, { status: 404 });
|
|
651
|
-
}
|
|
652
|
-
return NextResponse.json({ ok: true });
|
|
653
|
-
}
|
|
654
|
-
async function proxy(request, route) {
|
|
655
|
-
const accessToken = await getAccessToken();
|
|
656
|
-
const body = ["GET", "HEAD"].includes(request.method) ? void 0 : await readBody(request);
|
|
657
|
-
const url = new URL(appendSearchParams(createUrl(config.issuer, mapLocalPathToApiPath(route)), request));
|
|
658
|
-
applyClientCredentials(url, config);
|
|
659
|
-
const requestHeaders = new Headers();
|
|
660
|
-
if (accessToken) requestHeaders.set("authorization", `Bearer ${accessToken}`);
|
|
661
|
-
if (body !== void 0) requestHeaders.set("content-type", "application/json");
|
|
662
|
-
let response = await fetch(url, {
|
|
663
|
-
method: request.method,
|
|
664
|
-
headers: requestHeaders,
|
|
665
|
-
body: body === void 0 ? void 0 : typeof body === "string" ? body : JSON.stringify(body),
|
|
666
|
-
cache: "no-store"
|
|
667
|
-
});
|
|
668
|
-
if (response.status === 401) {
|
|
669
|
-
const refreshed = await tryRefreshTokens();
|
|
670
|
-
if (refreshed) {
|
|
671
|
-
requestHeaders.set("authorization", `Bearer ${refreshed}`);
|
|
672
|
-
response = await fetch(url, {
|
|
673
|
-
method: request.method,
|
|
674
|
-
headers: requestHeaders,
|
|
675
|
-
body: body === void 0 ? void 0 : typeof body === "string" ? body : JSON.stringify(body),
|
|
676
|
-
cache: "no-store"
|
|
677
|
-
});
|
|
678
|
-
} else {
|
|
679
|
-
await clearAllSessionCookies().catch(() => {
|
|
680
|
-
});
|
|
681
|
-
return NextResponse.json(
|
|
682
|
-
{ error: "session_expired" },
|
|
683
|
-
{ status: 401 }
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
const data = isJsonResponse(response) ? await response.json().catch(() => null) : null;
|
|
688
|
-
if (data) {
|
|
689
|
-
const tokens = tokenFromResponse(data);
|
|
690
|
-
if (tokens.accessToken || tokens.refreshToken || tokens.idToken || tokens.sessionId) {
|
|
691
|
-
await saveAccountToList(tokens);
|
|
692
|
-
}
|
|
693
|
-
return NextResponse.json(data, { status: response.status });
|
|
694
|
-
}
|
|
695
|
-
return responseFromApi(response);
|
|
696
|
-
}
|
|
697
|
-
async function handle(request) {
|
|
698
|
-
const route = routeFromRequest(request, config.basePath);
|
|
699
|
-
if (request.method === "GET" && route === "/sign-in") return createAuthorizeRedirect(request, "login");
|
|
700
|
-
if (request.method === "GET" && route === "/sign-up") return createAuthorizeRedirect(request, "register");
|
|
701
|
-
if (request.method === "GET" && route === "/add-account") return createAuthorizeRedirect(request, "add-account");
|
|
702
|
-
if (request.method === "GET" && route === "/login/google") return createAuthorizeRedirect(request, "google");
|
|
703
|
-
if (request.method === "GET" && route === "/callback") {
|
|
704
|
-
const error = request.nextUrl.searchParams.get("error");
|
|
705
|
-
if (error) {
|
|
706
|
-
const description = request.nextUrl.searchParams.get("error_description") ?? request.nextUrl.searchParams.get("error_message") ?? "An authentication error occurred.";
|
|
707
|
-
return new NextResponse(
|
|
708
|
-
`<!doctype html><html><head><meta charset="utf-8"><title>Authentication Error</title></head><body style="font-family:sans-serif;max-width:480px;margin:80px auto;padding:0 16px"><h2>Authentication Error</h2><p><strong>${escapeHtml(error)}</strong></p><p>${escapeHtml(description)}</p><a href="/">Go back</a></body></html>`,
|
|
709
|
-
{ status: 400, headers: { "content-type": "text/html; charset=utf-8" } }
|
|
710
|
-
);
|
|
711
|
-
}
|
|
712
|
-
return exchangeCode(request);
|
|
713
|
-
}
|
|
714
|
-
if (request.method === "GET" && route === "/token") {
|
|
715
|
-
const accessToken = await getAccessToken();
|
|
716
|
-
if (!accessToken) return NextResponse.json({ error: "missing_token" }, { status: 401 });
|
|
717
|
-
return NextResponse.json({ accessToken });
|
|
718
|
-
}
|
|
719
|
-
if (request.method === "GET" && route === "/accounts") return handleAccountsLocal();
|
|
720
|
-
if (request.method === "POST" && route === "/accounts/switch") return handleSwitchAccountLocal(request);
|
|
721
|
-
if (request.method === "GET" && route === "/organizations") return handleOrganizationsLocal();
|
|
722
|
-
if (request.method === "POST" && route === "/organizations") {
|
|
723
|
-
const accessToken = await getAccessToken();
|
|
724
|
-
const body = await readBody(request);
|
|
725
|
-
if (body && accessToken && !body.applicationId) {
|
|
726
|
-
const claims = decodeJwtPayload(accessToken);
|
|
727
|
-
const applicationId = (typeof claims?.application_id === "string" ? claims.application_id : null) ?? (typeof claims?.client_id === "string" ? claims.client_id : null) ?? (typeof claims?.applicationId === "string" ? claims.applicationId : null);
|
|
728
|
-
if (applicationId) body.applicationId = applicationId;
|
|
729
|
-
}
|
|
730
|
-
const orgUrl = new URL(createUrl(config.issuer, "/organizations"));
|
|
731
|
-
applyClientCredentials(orgUrl, config);
|
|
732
|
-
const requestHeaders = new Headers();
|
|
733
|
-
if (accessToken) requestHeaders.set("authorization", `Bearer ${accessToken}`);
|
|
734
|
-
if (body !== void 0) requestHeaders.set("content-type", "application/json");
|
|
735
|
-
const response = await fetch(orgUrl, {
|
|
736
|
-
method: "POST",
|
|
737
|
-
headers: requestHeaders,
|
|
738
|
-
body: JSON.stringify(body),
|
|
739
|
-
cache: "no-store"
|
|
740
|
-
});
|
|
741
|
-
return responseFromApi(response);
|
|
742
|
-
}
|
|
743
|
-
if (request.method === "POST" && route === "/sign-out") {
|
|
744
|
-
await apiRequest("/auth/sign-out", { method: "POST" }).catch(() => null);
|
|
745
|
-
await removeActiveAccount();
|
|
746
|
-
return NextResponse.json({ ok: true });
|
|
747
|
-
}
|
|
748
|
-
if (request.method === "POST" && route === "/sign-out-all") {
|
|
749
|
-
await apiRequest("/auth/sign-out-all", { method: "POST" }).catch(() => null);
|
|
750
|
-
await clearAllSessionCookies();
|
|
751
|
-
return NextResponse.json({ ok: true });
|
|
752
|
-
}
|
|
753
|
-
if (request.method === "POST" && route === "/organizations/active") {
|
|
754
|
-
const body = await readBody(request);
|
|
755
|
-
const organizationId = body && typeof body === "object" && "organizationId" in body ? body.organizationId : null;
|
|
756
|
-
const response = NextResponse.json({ ok: true });
|
|
757
|
-
if (typeof organizationId === "string") {
|
|
758
|
-
const cookieOpts = {
|
|
759
|
-
httpOnly: true,
|
|
760
|
-
sameSite: "lax",
|
|
761
|
-
path: "/",
|
|
762
|
-
secure: process.env.NODE_ENV === "production",
|
|
763
|
-
maxAge: 60 * 60 * 24 * 7
|
|
764
|
-
};
|
|
765
|
-
response.cookies.set(cn.activeOrganizationId, organizationId, cookieOpts);
|
|
766
|
-
}
|
|
767
|
-
return response;
|
|
768
|
-
}
|
|
769
|
-
return proxy(request, route);
|
|
770
|
-
}
|
|
771
|
-
return {
|
|
772
|
-
handlers: {
|
|
773
|
-
GET: handle,
|
|
774
|
-
POST: handle,
|
|
775
|
-
PATCH: handle,
|
|
776
|
-
DELETE: handle
|
|
777
|
-
},
|
|
778
|
-
GetSession: getSession,
|
|
779
|
-
GetUser: getUser,
|
|
780
|
-
GetUserById: getUserById,
|
|
781
|
-
GetOrganizations: getOrganizations,
|
|
782
|
-
SaveSession: saveAccountToList,
|
|
783
|
-
GetAccessToken: getAccessToken,
|
|
784
|
-
GetRefreshToken: getRefreshToken,
|
|
785
|
-
IsAuthenticated: async () => await getAccessToken() !== null,
|
|
786
|
-
TryRefreshTokens: tryRefreshTokens,
|
|
787
|
-
RequireUser: requireUser,
|
|
788
|
-
CreateAuthApiClient: createApiClient,
|
|
789
|
-
cookiePrefix: config.cookiePrefix,
|
|
790
|
-
VerifyToken: (token) => verifyToken(token, config.issuer),
|
|
791
|
-
SignInUrl: async (redirectTo) => {
|
|
792
|
-
const configured = env("GEEKAPPS_APP_URL");
|
|
793
|
-
if (!configured) throw new Error("Cannot determine app origin. Set GEEKAPPS_APP_URL.");
|
|
794
|
-
const origin = configured.replace(/\/$/, "");
|
|
795
|
-
const url = new URL(`${origin}${config.basePath}/sign-in`);
|
|
796
|
-
if (redirectTo) url.searchParams.set("redirectTo", redirectTo);
|
|
797
|
-
return url.toString();
|
|
798
|
-
}
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
13
|
|
|
802
14
|
// src/connections.ts
|
|
803
15
|
var AppConnectionError = class extends Error {
|
|
@@ -812,169 +24,28 @@ var AppConnectionError = class extends Error {
|
|
|
812
24
|
this.body = body;
|
|
813
25
|
}
|
|
814
26
|
};
|
|
815
|
-
function
|
|
27
|
+
function env(name) {
|
|
816
28
|
const value = process.env[name];
|
|
817
29
|
return value && value.trim().length > 0 ? value : void 0;
|
|
818
30
|
}
|
|
819
31
|
function resolveConfig(config) {
|
|
820
|
-
const issuer = (config.issuer ??
|
|
32
|
+
const issuer = (config.issuer ?? env("GEEKAPPS_AUTH_SERVER_ISSUER") ?? env("GEEKAPPS_AUTH_SERVER_URL") ?? GEEKAPPS_AUTH_SERVER_URL).replace(/\/$/, "");
|
|
821
33
|
return {
|
|
822
34
|
issuer,
|
|
823
|
-
clientId: config.clientId ??
|
|
824
|
-
clientSecret: config.clientSecret ??
|
|
35
|
+
clientId: config.clientId ?? env("GEEKAPPS_AUTH_CLIENT_ID") ?? "",
|
|
36
|
+
clientSecret: config.clientSecret ?? env("GEEKAPPS_AUTH_CLIENT_SECRET") ?? ""
|
|
825
37
|
};
|
|
826
38
|
}
|
|
827
|
-
|
|
828
|
-
const
|
|
829
|
-
if (!clientId || !clientSecret) {
|
|
830
|
-
throw new Error(
|
|
831
|
-
"App Connections: defina GEEKAPPS_AUTH_CLIENT_ID e GEEKAPPS_AUTH_CLIENT_SECRET \u2014 \xE9 com essas credenciais que o proxy identifica a app que est\xE1 chamando."
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
const userToken = params.userToken ?? await configureGeekappsAuth().GetAccessToken();
|
|
835
|
-
if (!userToken) {
|
|
836
|
-
throw new AppConnectionError(
|
|
837
|
-
"App Connections: n\xE3o h\xE1 usu\xE1rio logado \u2014 a conex\xE3o \xE9 do usu\xE1rio, a chamada precisa do access token dele.",
|
|
838
|
-
401,
|
|
839
|
-
"user_token_required",
|
|
840
|
-
null
|
|
841
|
-
);
|
|
842
|
-
}
|
|
843
|
-
const url = `${issuer}/connections/${encodeURIComponent(params.targetAppId)}/actions/${encodeURIComponent(params.actionKey)}`;
|
|
844
|
-
const response = await fetch(url, {
|
|
845
|
-
method: "POST",
|
|
846
|
-
headers: { "content-type": "application/json" },
|
|
847
|
-
// Credenciais da app e token do usuário vão no corpo, não em headers.
|
|
848
|
-
body: JSON.stringify({
|
|
849
|
-
client_id: clientId,
|
|
850
|
-
client_secret: clientSecret,
|
|
851
|
-
user_token: userToken,
|
|
852
|
-
...params.payload === void 0 ? {} : { payload: params.payload }
|
|
853
|
-
}),
|
|
854
|
-
cache: "no-store"
|
|
855
|
-
});
|
|
856
|
-
const body = await response.json().catch(() => null);
|
|
857
|
-
if (!response.ok) {
|
|
858
|
-
const code = body && typeof body === "object" && typeof body.error === "string" ? body.error : void 0;
|
|
859
|
-
throw new AppConnectionError(
|
|
860
|
-
`App Connections: ${params.actionKey} falhou (${response.status}${code ? ` ${code}` : ""})`,
|
|
861
|
-
response.status,
|
|
862
|
-
code,
|
|
863
|
-
body
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
return body;
|
|
867
|
-
}
|
|
868
|
-
var _registry = /* @__PURE__ */ new Map();
|
|
869
|
-
function registerConnections(entries) {
|
|
870
|
-
for (const [name, entry] of Object.entries(entries)) _registry.set(name, entry);
|
|
871
|
-
}
|
|
872
|
-
var TYPES_MODULE = "@geekapps/auth-nextjs/connections";
|
|
873
|
-
var TYPES_FILE = "geekapps-connections.d.ts";
|
|
874
|
-
var REFRESH_MIN_INTERVAL_MS = 5e3;
|
|
875
|
-
var _remote = /* @__PURE__ */ new Map();
|
|
876
|
-
var _inflight = /* @__PURE__ */ new Map();
|
|
877
|
-
async function writeTypesInDev(model) {
|
|
878
|
-
if (process.env.NODE_ENV !== "development" || process.env.GEEKAPPS_CONNECTIONS_TYPES === "off") return;
|
|
879
|
-
try {
|
|
880
|
-
const { existsSync, readFileSync, writeFileSync } = await import("fs");
|
|
881
|
-
const { resolve } = await import("path");
|
|
882
|
-
const file = resolve(process.cwd(), TYPES_FILE);
|
|
883
|
-
const content = renderAugmentation(model, TYPES_MODULE);
|
|
884
|
-
if (existsSync(file) && readFileSync(file, "utf-8") === content) return;
|
|
885
|
-
writeFileSync(file, content, "utf-8");
|
|
886
|
-
} catch {
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
async function loadRemoteRegistry(config, force = false) {
|
|
890
|
-
const { issuer, clientId, clientSecret } = resolveConfig(config);
|
|
891
|
-
if (!clientId || !clientSecret) {
|
|
892
|
-
throw new Error(
|
|
893
|
-
"App Connections: defina GEEKAPPS_AUTH_CLIENT_ID e GEEKAPPS_AUTH_CLIENT_SECRET \u2014 \xE9 com essas credenciais que o SDK descobre as apps conect\xE1veis."
|
|
894
|
-
);
|
|
895
|
-
}
|
|
896
|
-
const cacheKey = `${issuer}|${clientId}`;
|
|
897
|
-
const ttl = process.env.NODE_ENV === "development" ? 3e4 : 5 * 6e4;
|
|
898
|
-
const cached = _remote.get(cacheKey);
|
|
899
|
-
const age = cached ? Date.now() - cached.at : Infinity;
|
|
900
|
-
if (cached && (force ? age < REFRESH_MIN_INTERVAL_MS : age < ttl)) return cached.registry;
|
|
901
|
-
const pending = _inflight.get(cacheKey);
|
|
902
|
-
if (pending) return pending;
|
|
903
|
-
const request = (async () => {
|
|
904
|
-
try {
|
|
905
|
-
const model = buildModel(await fetchRemoteCatalog({ issuer, clientId, clientSecret }));
|
|
906
|
-
const registry = new Map(Object.entries(toRegistry(model)));
|
|
907
|
-
_remote.set(cacheKey, { at: Date.now(), registry });
|
|
908
|
-
void writeTypesInDev(model);
|
|
909
|
-
return registry;
|
|
910
|
-
} catch (error) {
|
|
911
|
-
if (cached) return cached.registry;
|
|
912
|
-
throw new AppConnectionError(`App Connections: ${error.message}`, 502, "catalog_unavailable", null);
|
|
913
|
-
} finally {
|
|
914
|
-
_inflight.delete(cacheKey);
|
|
915
|
-
}
|
|
916
|
-
})();
|
|
917
|
-
_inflight.set(cacheKey, request);
|
|
918
|
-
return request;
|
|
919
|
-
}
|
|
920
|
-
async function resolveTarget(provider, appName, actionName) {
|
|
921
|
-
for (const force of [false, true]) {
|
|
922
|
-
const registry = await provider(force);
|
|
923
|
-
const entry = registry.get(appName);
|
|
924
|
-
const key = entry?.actions[actionName];
|
|
925
|
-
if (entry && key) return { appId: entry.appId, key };
|
|
39
|
+
function requireCredentials(config) {
|
|
40
|
+
const resolved = resolveConfig(config);
|
|
41
|
+
if (!resolved.clientId || !resolved.clientSecret) {
|
|
42
|
+
throw new Error("App Connections: defina GEEKAPPS_AUTH_CLIENT_ID e GEEKAPPS_AUTH_CLIENT_SECRET.");
|
|
926
43
|
}
|
|
927
|
-
|
|
928
|
-
`App Connections: "${appName}.${actionName}" n\xE3o existe no cat\xE1logo. Confira se a app alvo cadastrou a a\xE7\xE3o (geekapps-actions sync).`
|
|
929
|
-
);
|
|
44
|
+
return resolved;
|
|
930
45
|
}
|
|
931
|
-
function
|
|
932
|
-
return
|
|
933
|
-
get(_target, appName) {
|
|
934
|
-
if (typeof appName !== "string") return void 0;
|
|
935
|
-
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
936
|
-
get(_t, actionName) {
|
|
937
|
-
if (typeof actionName !== "string") return void 0;
|
|
938
|
-
return async (payload, options) => {
|
|
939
|
-
const target = await resolveTarget(provider, appName, actionName);
|
|
940
|
-
return callAppConnection({
|
|
941
|
-
targetAppId: target.appId,
|
|
942
|
-
actionKey: target.key,
|
|
943
|
-
payload,
|
|
944
|
-
userToken: options?.userToken,
|
|
945
|
-
config
|
|
946
|
-
});
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
});
|
|
46
|
+
function errorCode(body) {
|
|
47
|
+
return body && typeof body === "object" && typeof body.error === "string" ? body.error : void 0;
|
|
952
48
|
}
|
|
953
|
-
var _devRefresh = /* @__PURE__ */ new Set();
|
|
954
|
-
function startDevRefresh(config) {
|
|
955
|
-
if (process.env.NODE_ENV !== "development") return;
|
|
956
|
-
const { issuer, clientId, clientSecret } = resolveConfig(config);
|
|
957
|
-
if (!clientId || !clientSecret) return;
|
|
958
|
-
const key = `${issuer}|${clientId}`;
|
|
959
|
-
if (_devRefresh.has(key)) return;
|
|
960
|
-
_devRefresh.add(key);
|
|
961
|
-
const tick = () => void loadRemoteRegistry(config, true).catch(() => void 0);
|
|
962
|
-
tick();
|
|
963
|
-
setInterval(tick, 3e4).unref?.();
|
|
964
|
-
}
|
|
965
|
-
function createAppConnections(config = {}) {
|
|
966
|
-
startDevRefresh(config);
|
|
967
|
-
const provider = async (force) => {
|
|
968
|
-
const remote = await loadRemoteRegistry(config, force);
|
|
969
|
-
return new Map([..._registry, ...remote]);
|
|
970
|
-
};
|
|
971
|
-
return createCatalog(config, provider);
|
|
972
|
-
}
|
|
973
|
-
function defineAppConnections(catalog, config = {}) {
|
|
974
|
-
const registry = new Map(Object.entries(catalog));
|
|
975
|
-
return createCatalog(config, async () => registry);
|
|
976
|
-
}
|
|
977
|
-
var appConnections = createAppConnections();
|
|
978
49
|
var CONNECT_COOKIES = {
|
|
979
50
|
verifier: "gk_conn_pkce",
|
|
980
51
|
state: "gk_conn_state",
|
|
@@ -986,9 +57,6 @@ async function connectApp(params) {
|
|
|
986
57
|
if (!clientId) {
|
|
987
58
|
throw new Error("App Connections: defina GEEKAPPS_AUTH_CLIENT_ID \u2014 \xE9 o client_id desta app, a que pede a conex\xE3o.");
|
|
988
59
|
}
|
|
989
|
-
if (params.actions.length === 0) {
|
|
990
|
-
throw new Error("App Connections: informe ao menos uma a\xE7\xE3o em `actions`.");
|
|
991
|
-
}
|
|
992
60
|
const codeVerifier = randomBytes2(64).toString("base64url");
|
|
993
61
|
const state = randomBytes2(24).toString("base64url");
|
|
994
62
|
const store = await cookies2();
|
|
@@ -1001,7 +69,7 @@ async function connectApp(params) {
|
|
|
1001
69
|
url.searchParams.set("client_id", clientId);
|
|
1002
70
|
url.searchParams.set("redirect_uri", params.redirectUri);
|
|
1003
71
|
url.searchParams.set("response_type", "code");
|
|
1004
|
-
url.searchParams.set("scope",
|
|
72
|
+
url.searchParams.set("scope", "openid");
|
|
1005
73
|
url.searchParams.set("state", state);
|
|
1006
74
|
url.searchParams.set("code_challenge", createHash2("sha256").update(codeVerifier).digest("base64url"));
|
|
1007
75
|
url.searchParams.set("code_challenge_method", "S256");
|
|
@@ -1011,10 +79,7 @@ async function connectApp(params) {
|
|
|
1011
79
|
redirect2(url.toString());
|
|
1012
80
|
}
|
|
1013
81
|
async function completeAppConnection(params) {
|
|
1014
|
-
const { issuer, clientId, clientSecret } =
|
|
1015
|
-
if (!clientId || !clientSecret) {
|
|
1016
|
-
throw new Error("App Connections: defina GEEKAPPS_AUTH_CLIENT_ID e GEEKAPPS_AUTH_CLIENT_SECRET.");
|
|
1017
|
-
}
|
|
82
|
+
const { issuer, clientId, clientSecret } = requireCredentials(params.config ?? {});
|
|
1018
83
|
const store = await cookies2();
|
|
1019
84
|
const read = (name) => store.get(name)?.value;
|
|
1020
85
|
const expectedState = read(CONNECT_COOKIES.state);
|
|
@@ -1039,9 +104,9 @@ async function completeAppConnection(params) {
|
|
|
1039
104
|
}),
|
|
1040
105
|
cache: "no-store"
|
|
1041
106
|
});
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const code = body
|
|
107
|
+
const body = await response.json().catch(() => null);
|
|
108
|
+
if (!response.ok || !body?.connection_token) {
|
|
109
|
+
const code = errorCode(body);
|
|
1045
110
|
throw new AppConnectionError(
|
|
1046
111
|
`App Connections: n\xE3o foi poss\xEDvel concluir a conex\xE3o (${response.status}${code ? ` ${code}` : ""})`,
|
|
1047
112
|
response.status,
|
|
@@ -1049,16 +114,128 @@ async function completeAppConnection(params) {
|
|
|
1049
114
|
body
|
|
1050
115
|
);
|
|
1051
116
|
}
|
|
1052
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
returnTo,
|
|
119
|
+
connection: {
|
|
120
|
+
token: body.connection_token,
|
|
121
|
+
connectionId: body.connection_id,
|
|
122
|
+
expiresAt: body.connection_expires_at ?? null,
|
|
123
|
+
organizationId: body.connection_organization_id,
|
|
124
|
+
targetAppId: body.connection_target_app_id
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
var _jwtCache = /* @__PURE__ */ new Map();
|
|
129
|
+
async function postToAuthServer(path, connectionToken, config) {
|
|
130
|
+
const { issuer, clientId, clientSecret } = requireCredentials(config);
|
|
131
|
+
const response = await fetch(`${issuer}${path}`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: {
|
|
134
|
+
authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
|
|
135
|
+
"content-type": "application/json"
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({ connection_token: connectionToken }),
|
|
138
|
+
cache: "no-store"
|
|
139
|
+
});
|
|
140
|
+
const body = await response.json().catch(() => null);
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const code = errorCode(body);
|
|
143
|
+
throw new AppConnectionError(
|
|
144
|
+
`App Connections: ${path} falhou (${response.status}${code ? ` ${code}` : ""})`,
|
|
145
|
+
response.status,
|
|
146
|
+
code,
|
|
147
|
+
body
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return body;
|
|
151
|
+
}
|
|
152
|
+
async function getConnectionAccessToken(params) {
|
|
153
|
+
const cached = _jwtCache.get(params.connectionToken);
|
|
154
|
+
if (cached && cached.expiresAt > Date.now() + 3e4) return cached.value;
|
|
155
|
+
const data = await postToAuthServer("/connections/token", params.connectionToken, params.config ?? {});
|
|
156
|
+
const value = {
|
|
157
|
+
accessToken: data.access_token,
|
|
158
|
+
expiresIn: data.expires_in,
|
|
159
|
+
organizationId: data.organization_id,
|
|
160
|
+
targetAppId: data.target_app_id,
|
|
161
|
+
targetApiBaseUrl: data.target_api_base_url,
|
|
162
|
+
targetOpenApiUrl: data.target_openapi_url
|
|
163
|
+
};
|
|
164
|
+
_jwtCache.set(params.connectionToken, { value, expiresAt: Date.now() + data.expires_in * 1e3 });
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
async function connectionFetch(params) {
|
|
168
|
+
const { accessToken, targetApiBaseUrl } = await getConnectionAccessToken({
|
|
169
|
+
connectionToken: params.connectionToken,
|
|
170
|
+
config: params.config
|
|
171
|
+
});
|
|
172
|
+
const isAbsolute = /^https?:\/\//i.test(params.path);
|
|
173
|
+
if (!isAbsolute && !targetApiBaseUrl) {
|
|
174
|
+
throw new Error("App Connections: a app alvo n\xE3o declarou apiBaseUrl \u2014 passe `path` como URL absoluta.");
|
|
175
|
+
}
|
|
176
|
+
const headers2 = new Headers(params.init?.headers);
|
|
177
|
+
headers2.set("authorization", `Bearer ${accessToken}`);
|
|
178
|
+
return fetch(isAbsolute ? params.path : new URL(params.path, targetApiBaseUrl), { ...params.init, headers: headers2 });
|
|
179
|
+
}
|
|
180
|
+
async function createConnectionClient(params) {
|
|
181
|
+
const { default: createClient } = await import("openapi-fetch").catch(() => {
|
|
182
|
+
throw new Error("App Connections: instale `openapi-fetch` para usar createConnectionClient.");
|
|
183
|
+
});
|
|
184
|
+
const { targetApiBaseUrl } = await getConnectionAccessToken({
|
|
185
|
+
connectionToken: params.connectionToken,
|
|
186
|
+
config: params.config
|
|
187
|
+
});
|
|
188
|
+
const baseUrl = params.baseUrl ?? targetApiBaseUrl;
|
|
189
|
+
if (!baseUrl) {
|
|
190
|
+
throw new Error("App Connections: a app alvo n\xE3o declarou apiBaseUrl \u2014 passe `baseUrl`.");
|
|
191
|
+
}
|
|
192
|
+
return createClient({
|
|
193
|
+
...params.options,
|
|
194
|
+
baseUrl,
|
|
195
|
+
// O JWT vence em minutos: pega do cache (ou renova) a cada requisição.
|
|
196
|
+
fetch: async (request) => {
|
|
197
|
+
const { accessToken } = await getConnectionAccessToken({
|
|
198
|
+
connectionToken: params.connectionToken,
|
|
199
|
+
config: params.config
|
|
200
|
+
});
|
|
201
|
+
request.headers.set("authorization", `Bearer ${accessToken}`);
|
|
202
|
+
return fetch(request);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async function rotateConnectionToken(params) {
|
|
207
|
+
const data = await postToAuthServer(
|
|
208
|
+
"/connections/token/rotate",
|
|
209
|
+
params.connectionToken,
|
|
210
|
+
params.config ?? {}
|
|
211
|
+
);
|
|
212
|
+
_jwtCache.delete(params.connectionToken);
|
|
213
|
+
return { token: data.connection_token, expiresAt: data.expires_at };
|
|
214
|
+
}
|
|
215
|
+
var TYPES_AUGMENT_MODULE = "@geekapps/auth-nextjs/connections";
|
|
216
|
+
async function syncTypes() {
|
|
217
|
+
const { issuer, clientId, clientSecret } = resolveConfig({});
|
|
218
|
+
if (!clientId || !clientSecret) return;
|
|
219
|
+
try {
|
|
220
|
+
const { generateConnectionTypes } = await import("@geekapps/auth-cli/lib");
|
|
221
|
+
await generateConnectionTypes({
|
|
222
|
+
credentials: { issuer, clientId, clientSecret },
|
|
223
|
+
augment: TYPES_AUGMENT_MODULE
|
|
224
|
+
});
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (process.env.NODE_ENV === "development" && process.env.GEEKAPPS_CONNECTIONS_TYPES !== "off") {
|
|
229
|
+
void syncTypes();
|
|
230
|
+
setInterval(() => void syncTypes(), 6e4).unref?.();
|
|
1053
231
|
}
|
|
1054
232
|
export {
|
|
1055
233
|
AppConnectionError,
|
|
1056
|
-
appConnections,
|
|
1057
|
-
callAppConnection,
|
|
1058
234
|
completeAppConnection,
|
|
1059
235
|
connectApp,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
236
|
+
connectionFetch,
|
|
237
|
+
createConnectionClient,
|
|
238
|
+
getConnectionAccessToken,
|
|
239
|
+
rotateConnectionToken
|
|
1063
240
|
};
|
|
1064
241
|
//# sourceMappingURL=connections.js.map
|