@ghostly-solutions/auth 0.1.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 +127 -0
- package/dist/auth-client-CAHMjodm.d.ts +32 -0
- package/dist/auth-sdk-error-DKM7PyKC.d.ts +26 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +483 -0
- package/dist/index.js.map +1 -0
- package/dist/next.d.ts +47 -0
- package/dist/next.js +899 -0
- package/dist/next.js.map +1 -0
- package/dist/react.d.ts +50 -0
- package/dist/react.js +681 -0
- package/dist/react.js.map +1 -0
- package/docs/api-reference.md +145 -0
- package/docs/architecture.md +62 -0
- package/docs/development-and-ci.md +53 -0
- package/docs/index.md +22 -0
- package/docs/integration-guide.md +142 -0
- package/docs/overview.md +41 -0
- package/package.json +66 -0
package/dist/next.js
ADDED
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
// src/constants/auth-endpoints.ts
|
|
2
|
+
var authApiPrefix = "/v1/auth";
|
|
3
|
+
var authEndpoints = {
|
|
4
|
+
loginStart: `${authApiPrefix}/keycloak/login`,
|
|
5
|
+
validateKeycloakToken: `${authApiPrefix}/keycloak/validate`,
|
|
6
|
+
session: `${authApiPrefix}/me`,
|
|
7
|
+
logout: `${authApiPrefix}/logout`
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// src/constants/http-status.ts
|
|
11
|
+
var httpStatus = {
|
|
12
|
+
ok: 200,
|
|
13
|
+
found: 302,
|
|
14
|
+
noContent: 204,
|
|
15
|
+
badRequest: 400,
|
|
16
|
+
unauthorized: 401
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/errors/auth-sdk-error.ts
|
|
20
|
+
var AuthSdkError = class extends Error {
|
|
21
|
+
code;
|
|
22
|
+
details;
|
|
23
|
+
status;
|
|
24
|
+
constructor(payload) {
|
|
25
|
+
super(payload.message);
|
|
26
|
+
this.name = "AuthSdkError";
|
|
27
|
+
this.code = payload.code;
|
|
28
|
+
this.details = payload.details;
|
|
29
|
+
this.status = payload.status;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/types/auth-error-code.ts
|
|
34
|
+
var authErrorCode = {
|
|
35
|
+
callbackMissingToken: "callback_missing_token",
|
|
36
|
+
callbackInvalidToken: "callback_invalid_token",
|
|
37
|
+
callbackValidationFailed: "callback_validation_failed",
|
|
38
|
+
unauthorized: "unauthorized",
|
|
39
|
+
networkError: "network_error",
|
|
40
|
+
apiError: "api_error",
|
|
41
|
+
broadcastChannelUnsupported: "broadcast_channel_unsupported",
|
|
42
|
+
serverOriginResolutionFailed: "server_origin_resolution_failed"
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/core/object-guards.ts
|
|
46
|
+
function isObjectRecord(value) {
|
|
47
|
+
return typeof value === "object" && value !== null;
|
|
48
|
+
}
|
|
49
|
+
function isStringValue(value) {
|
|
50
|
+
return typeof value === "string";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/core/session-parser.ts
|
|
54
|
+
function isStringArray(value) {
|
|
55
|
+
return Array.isArray(value) && value.every((entry) => isStringValue(entry));
|
|
56
|
+
}
|
|
57
|
+
function isGhostlySession(value) {
|
|
58
|
+
if (!isObjectRecord(value)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return isStringValue(value.id) && isStringValue(value.username) && (value.firstName === null || isStringValue(value.firstName)) && (value.lastName === null || isStringValue(value.lastName)) && isStringValue(value.email) && isStringValue(value.role) && isStringArray(value.permissions);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/adapters/next/server-session.ts
|
|
65
|
+
var hostHeaderName = "x-forwarded-host";
|
|
66
|
+
var fallbackHostHeaderName = "host";
|
|
67
|
+
var forwardedProtoHeaderName = "x-forwarded-proto";
|
|
68
|
+
var defaultProtocol = "https";
|
|
69
|
+
var protocolSeparator = "://";
|
|
70
|
+
var forwardedSessionHeaderNames = ["cookie", "authorization", "x-request-id"];
|
|
71
|
+
function resolveServerOrigin(options) {
|
|
72
|
+
const host = options.headers.get(hostHeaderName) ?? options.headers.get(fallbackHostHeaderName);
|
|
73
|
+
if (!host) {
|
|
74
|
+
throw new AuthSdkError({
|
|
75
|
+
code: authErrorCode.serverOriginResolutionFailed,
|
|
76
|
+
details: null,
|
|
77
|
+
message: "Cannot resolve server origin from request headers.",
|
|
78
|
+
status: null
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const protocol = options.protocol ?? options.headers.get(forwardedProtoHeaderName) ?? defaultProtocol;
|
|
82
|
+
return `${protocol}${protocolSeparator}${host}`;
|
|
83
|
+
}
|
|
84
|
+
function forwardSessionHeaders(headers) {
|
|
85
|
+
const forwarded = new Headers();
|
|
86
|
+
for (const headerName of forwardedSessionHeaderNames) {
|
|
87
|
+
const value = headers.get(headerName);
|
|
88
|
+
if (value) {
|
|
89
|
+
forwarded.set(headerName, value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return forwarded;
|
|
93
|
+
}
|
|
94
|
+
function parseSessionPayload(payload) {
|
|
95
|
+
if (payload === null) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (!isGhostlySession(payload)) {
|
|
99
|
+
throw new AuthSdkError({
|
|
100
|
+
code: authErrorCode.apiError,
|
|
101
|
+
details: payload,
|
|
102
|
+
message: "Session payload has invalid shape.",
|
|
103
|
+
status: null
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return payload;
|
|
107
|
+
}
|
|
108
|
+
async function getServerSession(options) {
|
|
109
|
+
const fetchImplementation = options.fetchImplementation ?? fetch;
|
|
110
|
+
const origin = resolveServerOrigin(options);
|
|
111
|
+
const response = await fetchImplementation(`${origin}${authEndpoints.session}`, {
|
|
112
|
+
cache: "no-store",
|
|
113
|
+
credentials: "include",
|
|
114
|
+
headers: forwardSessionHeaders(options.headers),
|
|
115
|
+
method: "GET"
|
|
116
|
+
});
|
|
117
|
+
if (response.status === httpStatus.ok) {
|
|
118
|
+
const payload = await response.json();
|
|
119
|
+
return parseSessionPayload(payload);
|
|
120
|
+
}
|
|
121
|
+
if (response.status === httpStatus.unauthorized) {
|
|
122
|
+
throw new AuthSdkError({
|
|
123
|
+
code: authErrorCode.unauthorized,
|
|
124
|
+
details: null,
|
|
125
|
+
message: "Unauthorized server session request.",
|
|
126
|
+
status: response.status
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
throw new AuthSdkError({
|
|
130
|
+
code: authErrorCode.apiError,
|
|
131
|
+
details: null,
|
|
132
|
+
message: "Unexpected server session response.",
|
|
133
|
+
status: response.status
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function requireServerSession(options) {
|
|
137
|
+
const session = await getServerSession(options);
|
|
138
|
+
if (session) {
|
|
139
|
+
return session;
|
|
140
|
+
}
|
|
141
|
+
throw new AuthSdkError({
|
|
142
|
+
code: authErrorCode.unauthorized,
|
|
143
|
+
details: null,
|
|
144
|
+
message: "Authenticated session is required.",
|
|
145
|
+
status: httpStatus.unauthorized
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/adapters/next/auth-kit.ts
|
|
150
|
+
var defaultCallbackToken = "mock-keycloak-token";
|
|
151
|
+
var defaultCookieName = "gs_auth_session";
|
|
152
|
+
var defaultCookieValue = "mock-session-super-admin";
|
|
153
|
+
var defaultFrontendCallbackPath = "/auth/callback";
|
|
154
|
+
var callbackCodePrefix = "mock_code_";
|
|
155
|
+
var callbackStatePrefix = "mock_state_";
|
|
156
|
+
var clearCookieDate = "Thu, 01 Jan 1970 00:00:00 GMT";
|
|
157
|
+
var proxyForwardedHeaderNames = [
|
|
158
|
+
"accept-language",
|
|
159
|
+
"authorization",
|
|
160
|
+
"cookie",
|
|
161
|
+
"content-type",
|
|
162
|
+
"x-request-id"
|
|
163
|
+
];
|
|
164
|
+
function defaultMockSessionFactory() {
|
|
165
|
+
return {
|
|
166
|
+
id: "123",
|
|
167
|
+
username: "john_doe",
|
|
168
|
+
firstName: "John",
|
|
169
|
+
lastName: "Doe",
|
|
170
|
+
email: "john.doe@ghostlysolutions.ae",
|
|
171
|
+
role: "superAdmin",
|
|
172
|
+
permissions: [
|
|
173
|
+
"users:view",
|
|
174
|
+
"tasks:view",
|
|
175
|
+
"analytics:view",
|
|
176
|
+
"settings:view",
|
|
177
|
+
"audit-logs:view",
|
|
178
|
+
"admins:view"
|
|
179
|
+
]
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function createErrorPayload(code, message, details = null) {
|
|
183
|
+
return { code, message, details };
|
|
184
|
+
}
|
|
185
|
+
function toJsonResponse(payload, status) {
|
|
186
|
+
return new Response(JSON.stringify(payload), {
|
|
187
|
+
status,
|
|
188
|
+
headers: {
|
|
189
|
+
"content-type": "application/json"
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function parseCookieValue(cookieHeader, cookieName) {
|
|
194
|
+
if (!cookieHeader) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const prefix = `${cookieName}=`;
|
|
198
|
+
const pairs = cookieHeader.split(";").map((pair) => pair.trim());
|
|
199
|
+
const match = pairs.find((pair) => pair.startsWith(prefix));
|
|
200
|
+
return match ? match.slice(prefix.length) : null;
|
|
201
|
+
}
|
|
202
|
+
function shouldForwardBody(method) {
|
|
203
|
+
const normalized = method.toUpperCase();
|
|
204
|
+
return normalized !== "GET" && normalized !== "HEAD";
|
|
205
|
+
}
|
|
206
|
+
function resolveSecureCookie(requestUrl, secureMode) {
|
|
207
|
+
if (typeof secureMode === "boolean") {
|
|
208
|
+
return secureMode;
|
|
209
|
+
}
|
|
210
|
+
if (requestUrl.protocol !== "https:") {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const hostname = requestUrl.hostname.toLowerCase();
|
|
214
|
+
return hostname !== "localhost" && hostname !== "127.0.0.1";
|
|
215
|
+
}
|
|
216
|
+
function toSetCookieHeader(options) {
|
|
217
|
+
const attributes = ["Path=/", "HttpOnly", "SameSite=Lax"];
|
|
218
|
+
if (options.secure) {
|
|
219
|
+
attributes.push("Secure");
|
|
220
|
+
}
|
|
221
|
+
if (options.clear) {
|
|
222
|
+
attributes.push("Max-Age=0");
|
|
223
|
+
attributes.push(`Expires=${clearCookieDate}`);
|
|
224
|
+
}
|
|
225
|
+
return `${options.cookieName}=${options.cookieValue}; ${attributes.join("; ")}`;
|
|
226
|
+
}
|
|
227
|
+
async function proxyRequest(baseUrl, request2) {
|
|
228
|
+
const incomingUrl = new URL(request2.url);
|
|
229
|
+
const targetUrl = new URL(incomingUrl.pathname + incomingUrl.search, baseUrl);
|
|
230
|
+
const proxyHeaders = new Headers();
|
|
231
|
+
for (const headerName of proxyForwardedHeaderNames) {
|
|
232
|
+
const value = request2.headers.get(headerName);
|
|
233
|
+
if (value) {
|
|
234
|
+
proxyHeaders.set(headerName, value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const body = shouldForwardBody(request2.method) ? await request2.text() : void 0;
|
|
238
|
+
const upstreamResponse = await fetch(targetUrl, {
|
|
239
|
+
method: request2.method,
|
|
240
|
+
headers: proxyHeaders,
|
|
241
|
+
cache: "no-store",
|
|
242
|
+
redirect: "manual",
|
|
243
|
+
...body !== void 0 ? { body } : {}
|
|
244
|
+
});
|
|
245
|
+
const responseHeaders = new Headers();
|
|
246
|
+
for (const [headerName, headerValue] of upstreamResponse.headers.entries()) {
|
|
247
|
+
responseHeaders.set(headerName, headerValue);
|
|
248
|
+
}
|
|
249
|
+
return new Response(upstreamResponse.body, {
|
|
250
|
+
status: upstreamResponse.status,
|
|
251
|
+
headers: responseHeaders
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async function resolveNextHeaders() {
|
|
255
|
+
try {
|
|
256
|
+
const importNextHeaders = new Function("return import('next/headers')");
|
|
257
|
+
const nextHeaders = await importNextHeaders();
|
|
258
|
+
const resolved = await nextHeaders.headers();
|
|
259
|
+
return resolved;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
throw new AuthSdkError({
|
|
262
|
+
code: authErrorCode.serverOriginResolutionFailed,
|
|
263
|
+
details: error,
|
|
264
|
+
message: "Unable to resolve Next request headers.",
|
|
265
|
+
status: null
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function resolveNextProtocol(headers, explicitProtocol) {
|
|
270
|
+
if (explicitProtocol) {
|
|
271
|
+
return explicitProtocol;
|
|
272
|
+
}
|
|
273
|
+
const forwarded = headers.get("x-forwarded-proto");
|
|
274
|
+
if (forwarded) {
|
|
275
|
+
return forwarded;
|
|
276
|
+
}
|
|
277
|
+
const host = headers.get("x-forwarded-host") ?? headers.get("host");
|
|
278
|
+
if (!host) {
|
|
279
|
+
return "https";
|
|
280
|
+
}
|
|
281
|
+
const normalizedHost = host.toLowerCase();
|
|
282
|
+
return normalizedHost.includes("localhost") || normalizedHost.includes("127.0.0.1") ? "http" : "https";
|
|
283
|
+
}
|
|
284
|
+
function normalizeMockToken(payload) {
|
|
285
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const recordPayload = payload;
|
|
289
|
+
const keys = Object.keys(recordPayload);
|
|
290
|
+
if (keys.length !== 1 || !keys.includes("token")) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const tokenValue = recordPayload.token;
|
|
294
|
+
if (typeof tokenValue !== "string") {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const normalized = tokenValue.trim();
|
|
298
|
+
return normalized.length > 0 ? normalized : null;
|
|
299
|
+
}
|
|
300
|
+
async function getNextServerSession(options = {}) {
|
|
301
|
+
const headers = options.headers ?? await resolveNextHeaders();
|
|
302
|
+
const protocol = resolveNextProtocol(headers, options.protocol);
|
|
303
|
+
return getServerSession({
|
|
304
|
+
headers,
|
|
305
|
+
protocol,
|
|
306
|
+
...options.fetchImplementation ? { fetchImplementation: options.fetchImplementation } : {}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async function tryGetNextServerSession(options = {}) {
|
|
310
|
+
try {
|
|
311
|
+
return await getNextServerSession(options);
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function requireNextServerSession(options = {}) {
|
|
317
|
+
const session = await getNextServerSession(options);
|
|
318
|
+
if (session) {
|
|
319
|
+
return session;
|
|
320
|
+
}
|
|
321
|
+
throw new AuthSdkError({
|
|
322
|
+
code: authErrorCode.unauthorized,
|
|
323
|
+
details: null,
|
|
324
|
+
message: "Authenticated session is required.",
|
|
325
|
+
status: httpStatus.unauthorized
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function createNextAuthRouteHandlers(options) {
|
|
329
|
+
const callbackToken = options.mock?.callbackToken ?? defaultCallbackToken;
|
|
330
|
+
const cookieName = options.mock?.cookieName ?? defaultCookieName;
|
|
331
|
+
const cookieValue = options.mock?.cookieValue ?? defaultCookieValue;
|
|
332
|
+
const cookieSecure = options.mock?.cookieSecure ?? "auto";
|
|
333
|
+
const frontendCallbackPath = options.mock?.frontendCallbackPath ?? defaultFrontendCallbackPath;
|
|
334
|
+
const createSession = options.mock?.createSession ?? defaultMockSessionFactory;
|
|
335
|
+
const proxyOptions = options.proxy;
|
|
336
|
+
if (options.mode === "proxy" && !proxyOptions?.baseUrl) {
|
|
337
|
+
throw new AuthSdkError({
|
|
338
|
+
code: authErrorCode.apiError,
|
|
339
|
+
details: null,
|
|
340
|
+
message: "proxy.baseUrl is required in proxy mode.",
|
|
341
|
+
status: null
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
const proxyIfNeeded = async (request2) => {
|
|
345
|
+
if (options.mode !== "proxy") {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
return proxyRequest(proxyOptions?.baseUrl ?? "", request2);
|
|
349
|
+
};
|
|
350
|
+
return {
|
|
351
|
+
keycloakLoginGet: async (request2) => {
|
|
352
|
+
const proxied = await proxyIfNeeded(request2);
|
|
353
|
+
if (proxied) {
|
|
354
|
+
return proxied;
|
|
355
|
+
}
|
|
356
|
+
const code = `${callbackCodePrefix}${crypto.randomUUID()}`;
|
|
357
|
+
const state = `${callbackStatePrefix}${crypto.randomUUID()}`;
|
|
358
|
+
const redirectTarget = `${authEndpoints.loginStart.replace("/login", "/callback")}?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
|
|
359
|
+
return new Response(null, {
|
|
360
|
+
status: httpStatus.found,
|
|
361
|
+
headers: {
|
|
362
|
+
location: redirectTarget
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
keycloakCallbackGet: async (request2) => {
|
|
367
|
+
const proxied = await proxyIfNeeded(request2);
|
|
368
|
+
if (proxied) {
|
|
369
|
+
return proxied;
|
|
370
|
+
}
|
|
371
|
+
const requestUrl = new URL(request2.url);
|
|
372
|
+
const code = requestUrl.searchParams.get("code")?.trim();
|
|
373
|
+
const state = requestUrl.searchParams.get("state")?.trim();
|
|
374
|
+
if (!(code && state)) {
|
|
375
|
+
return toJsonResponse(
|
|
376
|
+
createErrorPayload("bad_request", "code and state are required."),
|
|
377
|
+
httpStatus.badRequest
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const callbackUrl = `${frontendCallbackPath}?token=${encodeURIComponent(callbackToken)}`;
|
|
381
|
+
return new Response(null, {
|
|
382
|
+
status: httpStatus.found,
|
|
383
|
+
headers: {
|
|
384
|
+
location: callbackUrl
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
keycloakValidatePost: async (request2) => {
|
|
389
|
+
const proxied = await proxyIfNeeded(request2);
|
|
390
|
+
if (proxied) {
|
|
391
|
+
return proxied;
|
|
392
|
+
}
|
|
393
|
+
let payload;
|
|
394
|
+
try {
|
|
395
|
+
payload = await request2.json();
|
|
396
|
+
} catch {
|
|
397
|
+
return toJsonResponse(
|
|
398
|
+
createErrorPayload("bad_request", "Request payload must be valid JSON."),
|
|
399
|
+
httpStatus.badRequest
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
const token = normalizeMockToken(payload);
|
|
403
|
+
if (!token) {
|
|
404
|
+
return toJsonResponse(
|
|
405
|
+
createErrorPayload(
|
|
406
|
+
"bad_request",
|
|
407
|
+
"Request payload must contain only non-empty token field."
|
|
408
|
+
),
|
|
409
|
+
httpStatus.badRequest
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (token !== callbackToken) {
|
|
413
|
+
return toJsonResponse(
|
|
414
|
+
createErrorPayload("unauthorized", "Callback token is invalid or expired."),
|
|
415
|
+
httpStatus.unauthorized
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
const session = createSession();
|
|
419
|
+
const responsePayload = { session };
|
|
420
|
+
const secure = resolveSecureCookie(new URL(request2.url), cookieSecure);
|
|
421
|
+
return new Response(JSON.stringify(responsePayload), {
|
|
422
|
+
status: httpStatus.ok,
|
|
423
|
+
headers: {
|
|
424
|
+
"content-type": "application/json",
|
|
425
|
+
"set-cookie": toSetCookieHeader({
|
|
426
|
+
cookieName,
|
|
427
|
+
cookieValue,
|
|
428
|
+
secure
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
},
|
|
433
|
+
meGet: async (request2) => {
|
|
434
|
+
const proxied = await proxyIfNeeded(request2);
|
|
435
|
+
if (proxied) {
|
|
436
|
+
return proxied;
|
|
437
|
+
}
|
|
438
|
+
const cookie = request2.headers.get("cookie");
|
|
439
|
+
const session = parseCookieValue(cookie, cookieName) === cookieValue ? createSession() : null;
|
|
440
|
+
return toJsonResponse(session, httpStatus.ok);
|
|
441
|
+
},
|
|
442
|
+
logoutPost: async (request2) => {
|
|
443
|
+
const proxied = await proxyIfNeeded(request2);
|
|
444
|
+
if (proxied) {
|
|
445
|
+
return proxied;
|
|
446
|
+
}
|
|
447
|
+
const secure = resolveSecureCookie(new URL(request2.url), cookieSecure);
|
|
448
|
+
return new Response(null, {
|
|
449
|
+
status: httpStatus.noContent,
|
|
450
|
+
headers: {
|
|
451
|
+
"set-cookie": toSetCookieHeader({
|
|
452
|
+
cookieName,
|
|
453
|
+
cookieValue: "",
|
|
454
|
+
secure,
|
|
455
|
+
clear: true
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/constants/auth-keys.ts
|
|
464
|
+
var authQueryKeys = {
|
|
465
|
+
token: "token"
|
|
466
|
+
};
|
|
467
|
+
var authStorageKeys = {
|
|
468
|
+
returnTo: "ghostly-auth:return-to"
|
|
469
|
+
};
|
|
470
|
+
var authBroadcast = {
|
|
471
|
+
channelName: "ghostly-auth-channel",
|
|
472
|
+
sessionUpdatedEvent: "session-updated"
|
|
473
|
+
};
|
|
474
|
+
var authRoutes = {
|
|
475
|
+
root: "/"};
|
|
476
|
+
|
|
477
|
+
// src/core/broadcast-sync.ts
|
|
478
|
+
function isSessionUpdatedMessage(value) {
|
|
479
|
+
if (!isObjectRecord(value)) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
if (!isStringValue(value.type)) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
if (value.type !== authBroadcast.sessionUpdatedEvent) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
return value.session === null || isGhostlySession(value.session);
|
|
489
|
+
}
|
|
490
|
+
function createUnsupportedBroadcastChannelError() {
|
|
491
|
+
return new AuthSdkError({
|
|
492
|
+
code: authErrorCode.broadcastChannelUnsupported,
|
|
493
|
+
details: null,
|
|
494
|
+
message: "BroadcastChannel is unavailable in this runtime.",
|
|
495
|
+
status: null
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
function createBroadcastSync(options) {
|
|
499
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
500
|
+
throw createUnsupportedBroadcastChannelError();
|
|
501
|
+
}
|
|
502
|
+
const channel = new BroadcastChannel(authBroadcast.channelName);
|
|
503
|
+
const onMessage = (event) => {
|
|
504
|
+
const messageEvent = event;
|
|
505
|
+
if (!isSessionUpdatedMessage(messageEvent.data)) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
options.onSessionUpdated(messageEvent.data.session);
|
|
509
|
+
};
|
|
510
|
+
channel.addEventListener("message", onMessage);
|
|
511
|
+
return {
|
|
512
|
+
close() {
|
|
513
|
+
channel.removeEventListener("message", onMessage);
|
|
514
|
+
channel.close();
|
|
515
|
+
},
|
|
516
|
+
publishSession(session) {
|
|
517
|
+
const payload = {
|
|
518
|
+
session,
|
|
519
|
+
type: authBroadcast.sessionUpdatedEvent
|
|
520
|
+
};
|
|
521
|
+
channel.postMessage(payload);
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/core/callback-url.ts
|
|
527
|
+
function readCallbackToken(url) {
|
|
528
|
+
return url.searchParams.get(authQueryKeys.token);
|
|
529
|
+
}
|
|
530
|
+
function removeCallbackToken(url) {
|
|
531
|
+
const nextUrl = new URL(url.toString());
|
|
532
|
+
nextUrl.searchParams.delete(authQueryKeys.token);
|
|
533
|
+
return nextUrl;
|
|
534
|
+
}
|
|
535
|
+
function replaceBrowserHistory(url) {
|
|
536
|
+
window.history.replaceState(null, "", url.toString());
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/core/http-client.ts
|
|
540
|
+
var jsonContentType = "application/json";
|
|
541
|
+
var jsonHeaderName = "content-type";
|
|
542
|
+
var includeCredentials = "include";
|
|
543
|
+
var noStoreCache = "no-store";
|
|
544
|
+
function toTypedValue(value) {
|
|
545
|
+
return value;
|
|
546
|
+
}
|
|
547
|
+
function mapHttpStatusToAuthErrorCode(status) {
|
|
548
|
+
if (status === httpStatus.unauthorized) {
|
|
549
|
+
return authErrorCode.unauthorized;
|
|
550
|
+
}
|
|
551
|
+
return authErrorCode.apiError;
|
|
552
|
+
}
|
|
553
|
+
async function parseJsonPayload(response) {
|
|
554
|
+
try {
|
|
555
|
+
return await response.json();
|
|
556
|
+
} catch {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async function parseErrorPayload(response) {
|
|
561
|
+
const payload = await parseJsonPayload(response);
|
|
562
|
+
if (!isObjectRecord(payload)) {
|
|
563
|
+
return {
|
|
564
|
+
code: null,
|
|
565
|
+
details: null,
|
|
566
|
+
message: null
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const maybeCode = payload.code;
|
|
570
|
+
const maybeMessage = payload.message;
|
|
571
|
+
return {
|
|
572
|
+
code: isStringValue(maybeCode) ? maybeCode : null,
|
|
573
|
+
details: "details" in payload ? payload.details : null,
|
|
574
|
+
message: isStringValue(maybeMessage) ? maybeMessage : null
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
function buildApiErrorMessage(method, path) {
|
|
578
|
+
return `Auth API request failed: ${method} ${path}`;
|
|
579
|
+
}
|
|
580
|
+
function buildNetworkErrorMessage(method, path) {
|
|
581
|
+
return `Auth API network failure: ${method} ${path}`;
|
|
582
|
+
}
|
|
583
|
+
async function request(options) {
|
|
584
|
+
const expectedStatus = options.expectedStatus ?? httpStatus.ok;
|
|
585
|
+
const headers = new Headers();
|
|
586
|
+
const hasBody = typeof options.body !== "undefined";
|
|
587
|
+
if (hasBody) {
|
|
588
|
+
headers.set(jsonHeaderName, jsonContentType);
|
|
589
|
+
}
|
|
590
|
+
const requestInit = {
|
|
591
|
+
cache: noStoreCache,
|
|
592
|
+
credentials: includeCredentials,
|
|
593
|
+
headers,
|
|
594
|
+
method: options.method
|
|
595
|
+
};
|
|
596
|
+
if (hasBody) {
|
|
597
|
+
requestInit.body = JSON.stringify(options.body);
|
|
598
|
+
}
|
|
599
|
+
let response;
|
|
600
|
+
try {
|
|
601
|
+
response = await fetch(options.path, requestInit);
|
|
602
|
+
} catch (error) {
|
|
603
|
+
throw new AuthSdkError({
|
|
604
|
+
code: authErrorCode.networkError,
|
|
605
|
+
details: error,
|
|
606
|
+
message: buildNetworkErrorMessage(options.method, options.path),
|
|
607
|
+
status: null
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
if (response.status !== expectedStatus) {
|
|
611
|
+
const parsed = await parseErrorPayload(response);
|
|
612
|
+
throw new AuthSdkError({
|
|
613
|
+
code: mapHttpStatusToAuthErrorCode(response.status),
|
|
614
|
+
details: {
|
|
615
|
+
apiCode: parsed.code,
|
|
616
|
+
apiDetails: parsed.details
|
|
617
|
+
},
|
|
618
|
+
message: parsed.message ?? buildApiErrorMessage(options.method, options.path),
|
|
619
|
+
status: response.status
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
if (response.status === httpStatus.noContent) {
|
|
623
|
+
return toTypedValue(null);
|
|
624
|
+
}
|
|
625
|
+
const payload = await parseJsonPayload(response);
|
|
626
|
+
return toTypedValue(payload);
|
|
627
|
+
}
|
|
628
|
+
function getJson(path) {
|
|
629
|
+
return request({
|
|
630
|
+
method: "GET",
|
|
631
|
+
path
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
function postJson(path, body) {
|
|
635
|
+
return request({
|
|
636
|
+
body,
|
|
637
|
+
method: "POST",
|
|
638
|
+
path
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function postEmpty(path) {
|
|
642
|
+
return request({
|
|
643
|
+
expectedStatus: httpStatus.noContent,
|
|
644
|
+
method: "POST",
|
|
645
|
+
path
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/core/runtime.ts
|
|
650
|
+
var browserRuntimeErrorMessage = "Browser runtime is required for this auth operation.";
|
|
651
|
+
function isBrowserRuntime() {
|
|
652
|
+
return typeof window !== "undefined";
|
|
653
|
+
}
|
|
654
|
+
function assertBrowserRuntime() {
|
|
655
|
+
if (isBrowserRuntime()) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
throw new AuthSdkError({
|
|
659
|
+
code: authErrorCode.apiError,
|
|
660
|
+
details: null,
|
|
661
|
+
message: browserRuntimeErrorMessage,
|
|
662
|
+
status: null
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/core/return-to-storage.ts
|
|
667
|
+
function sanitizeReturnTo(value) {
|
|
668
|
+
if (!value) {
|
|
669
|
+
return authRoutes.root;
|
|
670
|
+
}
|
|
671
|
+
if (!value.startsWith(authRoutes.root)) {
|
|
672
|
+
return authRoutes.root;
|
|
673
|
+
}
|
|
674
|
+
const protocolRelativePrefix = "//";
|
|
675
|
+
if (value.startsWith(protocolRelativePrefix)) {
|
|
676
|
+
return authRoutes.root;
|
|
677
|
+
}
|
|
678
|
+
return value;
|
|
679
|
+
}
|
|
680
|
+
function getCurrentBrowserPath() {
|
|
681
|
+
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
682
|
+
}
|
|
683
|
+
function saveReturnToPath(returnTo) {
|
|
684
|
+
assertBrowserRuntime();
|
|
685
|
+
const fallbackPath = getCurrentBrowserPath();
|
|
686
|
+
const sanitized = sanitizeReturnTo(returnTo ?? fallbackPath);
|
|
687
|
+
window.sessionStorage.setItem(authStorageKeys.returnTo, sanitized);
|
|
688
|
+
return sanitized;
|
|
689
|
+
}
|
|
690
|
+
function consumeReturnToPath() {
|
|
691
|
+
assertBrowserRuntime();
|
|
692
|
+
const value = window.sessionStorage.getItem(authStorageKeys.returnTo);
|
|
693
|
+
window.sessionStorage.removeItem(authStorageKeys.returnTo);
|
|
694
|
+
return sanitizeReturnTo(value);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/core/session-store.ts
|
|
698
|
+
var SessionStore = class {
|
|
699
|
+
listeners = /* @__PURE__ */ new Set();
|
|
700
|
+
resolvedSession = null;
|
|
701
|
+
resolveState = "pending";
|
|
702
|
+
getSessionIfResolved() {
|
|
703
|
+
if (this.resolveState === "pending") {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
return this.resolvedSession;
|
|
707
|
+
}
|
|
708
|
+
hasResolvedSession() {
|
|
709
|
+
return this.resolveState === "resolved";
|
|
710
|
+
}
|
|
711
|
+
setSession(session) {
|
|
712
|
+
this.resolveState = "resolved";
|
|
713
|
+
this.resolvedSession = session;
|
|
714
|
+
for (const listener of this.listeners) {
|
|
715
|
+
listener(session);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
subscribe(listener) {
|
|
719
|
+
this.listeners.add(listener);
|
|
720
|
+
return () => {
|
|
721
|
+
this.listeners.delete(listener);
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
// src/core/auth-client.ts
|
|
727
|
+
function createPendingRedirectPromise() {
|
|
728
|
+
return new Promise(() => {
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
function createInvalidSessionPayloadError(path) {
|
|
732
|
+
return new AuthSdkError({
|
|
733
|
+
code: authErrorCode.apiError,
|
|
734
|
+
details: null,
|
|
735
|
+
message: `Auth API response has invalid session shape: ${path}`,
|
|
736
|
+
status: null
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function toValidatedSession(payload, path) {
|
|
740
|
+
if (!isGhostlySession(payload)) {
|
|
741
|
+
throw createInvalidSessionPayloadError(path);
|
|
742
|
+
}
|
|
743
|
+
return payload;
|
|
744
|
+
}
|
|
745
|
+
function toCallbackFailure(error) {
|
|
746
|
+
if (error instanceof AuthSdkError) {
|
|
747
|
+
if (error.status === httpStatus.unauthorized) {
|
|
748
|
+
return new AuthSdkError({
|
|
749
|
+
code: authErrorCode.callbackInvalidToken,
|
|
750
|
+
details: error.details,
|
|
751
|
+
message: "Callback JWT is invalid or expired.",
|
|
752
|
+
status: error.status
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
return new AuthSdkError({
|
|
756
|
+
code: authErrorCode.callbackValidationFailed,
|
|
757
|
+
details: error.details,
|
|
758
|
+
message: "Keycloak callback validation failed.",
|
|
759
|
+
status: error.status
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
return new AuthSdkError({
|
|
763
|
+
code: authErrorCode.callbackValidationFailed,
|
|
764
|
+
details: error,
|
|
765
|
+
message: "Keycloak callback validation failed.",
|
|
766
|
+
status: null
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
function createNoopBroadcastSync() {
|
|
770
|
+
return {
|
|
771
|
+
close() {
|
|
772
|
+
},
|
|
773
|
+
publishSession() {
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function createSafeBroadcastSync(onSessionUpdated) {
|
|
778
|
+
try {
|
|
779
|
+
return createBroadcastSync({
|
|
780
|
+
onSessionUpdated
|
|
781
|
+
});
|
|
782
|
+
} catch (error) {
|
|
783
|
+
if (error instanceof AuthSdkError && error.code === authErrorCode.broadcastChannelUnsupported) {
|
|
784
|
+
return createNoopBroadcastSync();
|
|
785
|
+
}
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async function fetchCurrentSessionFromApi() {
|
|
790
|
+
const payload = await getJson(authEndpoints.session);
|
|
791
|
+
if (payload === null) {
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
return toValidatedSession(payload, authEndpoints.session);
|
|
795
|
+
}
|
|
796
|
+
function createAuthClient() {
|
|
797
|
+
assertBrowserRuntime();
|
|
798
|
+
const sessionStore = new SessionStore();
|
|
799
|
+
const broadcastSync = createSafeBroadcastSync((session) => {
|
|
800
|
+
sessionStore.setSession(session);
|
|
801
|
+
});
|
|
802
|
+
const getSession = async (options) => {
|
|
803
|
+
const forceRefresh = options?.forceRefresh ?? false;
|
|
804
|
+
if (sessionStore.hasResolvedSession() && !forceRefresh) {
|
|
805
|
+
return sessionStore.getSessionIfResolved();
|
|
806
|
+
}
|
|
807
|
+
const session = await fetchCurrentSessionFromApi();
|
|
808
|
+
sessionStore.setSession(session);
|
|
809
|
+
broadcastSync.publishSession(session);
|
|
810
|
+
return session;
|
|
811
|
+
};
|
|
812
|
+
const requireSession = async () => {
|
|
813
|
+
const session = await getSession();
|
|
814
|
+
if (session) {
|
|
815
|
+
return session;
|
|
816
|
+
}
|
|
817
|
+
throw new AuthSdkError({
|
|
818
|
+
code: authErrorCode.unauthorized,
|
|
819
|
+
details: null,
|
|
820
|
+
message: "Authenticated session is required.",
|
|
821
|
+
status: httpStatus.unauthorized
|
|
822
|
+
});
|
|
823
|
+
};
|
|
824
|
+
const login = (options) => {
|
|
825
|
+
saveReturnToPath(options?.returnTo);
|
|
826
|
+
window.location.assign(authEndpoints.loginStart);
|
|
827
|
+
};
|
|
828
|
+
const processCallback = async () => {
|
|
829
|
+
const currentUrl = new URL(window.location.href);
|
|
830
|
+
const token = readCallbackToken(currentUrl);
|
|
831
|
+
if (!token) {
|
|
832
|
+
throw new AuthSdkError({
|
|
833
|
+
code: authErrorCode.callbackMissingToken,
|
|
834
|
+
details: null,
|
|
835
|
+
message: "Missing callback token query parameter.",
|
|
836
|
+
status: httpStatus.badRequest
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
const cleanedUrl = removeCallbackToken(currentUrl);
|
|
840
|
+
replaceBrowserHistory(cleanedUrl);
|
|
841
|
+
try {
|
|
842
|
+
const payload = await postJson(
|
|
843
|
+
authEndpoints.validateKeycloakToken,
|
|
844
|
+
{ token }
|
|
845
|
+
);
|
|
846
|
+
const session = toValidatedSession(payload.session, authEndpoints.validateKeycloakToken);
|
|
847
|
+
sessionStore.setSession(session);
|
|
848
|
+
broadcastSync.publishSession(session);
|
|
849
|
+
return {
|
|
850
|
+
redirectTo: consumeReturnToPath(),
|
|
851
|
+
session
|
|
852
|
+
};
|
|
853
|
+
} catch (error) {
|
|
854
|
+
throw toCallbackFailure(error);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
const completeCallbackRedirect = async () => {
|
|
858
|
+
const result = await processCallback();
|
|
859
|
+
window.location.replace(result.redirectTo);
|
|
860
|
+
return createPendingRedirectPromise();
|
|
861
|
+
};
|
|
862
|
+
const logout = async () => {
|
|
863
|
+
await postEmpty(authEndpoints.logout);
|
|
864
|
+
sessionStore.setSession(null);
|
|
865
|
+
broadcastSync.publishSession(null);
|
|
866
|
+
};
|
|
867
|
+
const subscribe = sessionStore.subscribe.bind(sessionStore);
|
|
868
|
+
return {
|
|
869
|
+
completeCallbackRedirect,
|
|
870
|
+
getSession,
|
|
871
|
+
login,
|
|
872
|
+
logout,
|
|
873
|
+
processCallback,
|
|
874
|
+
requireSession,
|
|
875
|
+
subscribe
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/adapters/next/client-guard.ts
|
|
880
|
+
function createPendingRedirectPromise2() {
|
|
881
|
+
return new Promise(() => {
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
async function ensureClientAuthenticated(client) {
|
|
885
|
+
const authClient = client ?? createAuthClient();
|
|
886
|
+
try {
|
|
887
|
+
return await authClient.requireSession();
|
|
888
|
+
} catch (error) {
|
|
889
|
+
if (error instanceof AuthSdkError && error.code === authErrorCode.unauthorized) {
|
|
890
|
+
authClient.login();
|
|
891
|
+
return createPendingRedirectPromise2();
|
|
892
|
+
}
|
|
893
|
+
throw error;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export { createNextAuthRouteHandlers, ensureClientAuthenticated, getNextServerSession, getServerSession, requireNextServerSession, requireServerSession, tryGetNextServerSession };
|
|
898
|
+
//# sourceMappingURL=next.js.map
|
|
899
|
+
//# sourceMappingURL=next.js.map
|