@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/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# @ghostly-solutions/auth
|
|
2
|
+
|
|
3
|
+
Authentication SDK for Ghostly Solutions products.
|
|
4
|
+
|
|
5
|
+
The SDK implements a fixed Keycloak redirect flow with Ghostly Auth API validation and typed session state.
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- Fixed API contract (no runtime endpoint configuration).
|
|
10
|
+
- Typed core client with deterministic error codes.
|
|
11
|
+
- Dedicated callback flow with immediate token URL cleanup.
|
|
12
|
+
- Session state with lazy revalidation.
|
|
13
|
+
- Cross-tab synchronization via `BroadcastChannel`.
|
|
14
|
+
- React adapter (`AuthProvider`, `useAuth`).
|
|
15
|
+
- React callback helpers (`AuthCallbackHandler`, `useAuthCallbackRedirect`).
|
|
16
|
+
- React session gate (`AuthSessionGate`).
|
|
17
|
+
- Next adapter (server and client guards).
|
|
18
|
+
- Next Auth Kit with ready route handlers (`mock` or `proxy`) and server-session helpers.
|
|
19
|
+
|
|
20
|
+
## Fixed API Endpoints
|
|
21
|
+
|
|
22
|
+
- `GET /v1/auth/keycloak/login`
|
|
23
|
+
- `POST /v1/auth/keycloak/validate`
|
|
24
|
+
- `GET /v1/auth/me`
|
|
25
|
+
- `POST /v1/auth/logout`
|
|
26
|
+
|
|
27
|
+
## Entry Points
|
|
28
|
+
|
|
29
|
+
- `@ghostly-solutions/auth`
|
|
30
|
+
- `@ghostly-solutions/auth/react`
|
|
31
|
+
- `@ghostly-solutions/auth/next`
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @ghostly-solutions/auth
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm run lint
|
|
43
|
+
npm run typecheck
|
|
44
|
+
npm run test
|
|
45
|
+
npm run build
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
All CI and official workflows are npm-first.
|
|
49
|
+
|
|
50
|
+
Bun is optional for local use through aliases in `bun.json`.
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { createAuthClient } from "@ghostly-solutions/auth";
|
|
56
|
+
|
|
57
|
+
const authClient = createAuthClient();
|
|
58
|
+
|
|
59
|
+
// Start login flow
|
|
60
|
+
authClient.login();
|
|
61
|
+
|
|
62
|
+
// In /auth/callback route
|
|
63
|
+
await authClient.completeCallbackRedirect();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Next.js "No-Glue" Integration
|
|
67
|
+
|
|
68
|
+
Use the high-level Next adapter in `@ghostly-solutions/auth/next`:
|
|
69
|
+
|
|
70
|
+
- `getNextServerSession()`
|
|
71
|
+
- `requireNextServerSession()`
|
|
72
|
+
- `createNextAuthRouteHandlers({ mode: "mock" | "proxy" })`
|
|
73
|
+
|
|
74
|
+
This removes custom auth boilerplate from application code and keeps app-level integration thin.
|
|
75
|
+
|
|
76
|
+
## Callback Page Helper
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { AuthCallbackHandler } from "@ghostly-solutions/auth/react";
|
|
80
|
+
|
|
81
|
+
export default function AuthCallbackPage() {
|
|
82
|
+
return (
|
|
83
|
+
<AuthCallbackHandler
|
|
84
|
+
processing={<div>Signing you in...</div>}
|
|
85
|
+
renderError={() => <div>Could not complete sign in.</div>}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Error Handling
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { AuthSdkError } from "@ghostly-solutions/auth";
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await authClient.requireSession();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof AuthSdkError) {
|
|
100
|
+
if (error.code === "unauthorized") {
|
|
101
|
+
authClient.login();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Documentation
|
|
108
|
+
|
|
109
|
+
- [Docs Index](./docs/index.md)
|
|
110
|
+
- [Overview](./docs/overview.md)
|
|
111
|
+
- [API Reference](./docs/api-reference.md)
|
|
112
|
+
- [Integration Guide](./docs/integration-guide.md)
|
|
113
|
+
- [Architecture](./docs/architecture.md)
|
|
114
|
+
- [Development and CI](./docs/development-and-ci.md)
|
|
115
|
+
|
|
116
|
+
## Interactive Demo
|
|
117
|
+
|
|
118
|
+
Run the simulated authentication page in this repository:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm run demo
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then open `http://localhost:4100/` to test login, callback processing, session retrieval,
|
|
125
|
+
and logout.
|
|
126
|
+
|
|
127
|
+
Detailed demo docs: [demo/README.md](./demo/README.md).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface GhostlySession {
|
|
2
|
+
id: string;
|
|
3
|
+
username: string;
|
|
4
|
+
firstName: string | null;
|
|
5
|
+
lastName: string | null;
|
|
6
|
+
email: string;
|
|
7
|
+
role: string;
|
|
8
|
+
permissions: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ProcessCallbackResult {
|
|
12
|
+
redirectTo: string;
|
|
13
|
+
session: GhostlySession;
|
|
14
|
+
}
|
|
15
|
+
interface LoginOptions {
|
|
16
|
+
returnTo?: string;
|
|
17
|
+
}
|
|
18
|
+
interface SessionRequestOptions {
|
|
19
|
+
forceRefresh?: boolean;
|
|
20
|
+
}
|
|
21
|
+
type SessionListener = (session: GhostlySession | null) => void;
|
|
22
|
+
interface AuthClient {
|
|
23
|
+
completeCallbackRedirect(): Promise<never>;
|
|
24
|
+
getSession(options?: SessionRequestOptions): Promise<GhostlySession | null>;
|
|
25
|
+
login(options?: LoginOptions): void;
|
|
26
|
+
logout(): Promise<void>;
|
|
27
|
+
processCallback(): Promise<ProcessCallbackResult>;
|
|
28
|
+
requireSession(): Promise<GhostlySession>;
|
|
29
|
+
subscribe(listener: SessionListener): () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type { AuthClient as A, GhostlySession as G, LoginOptions as L, ProcessCallbackResult as P, SessionListener as S, SessionRequestOptions as a };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
declare const authErrorCode: {
|
|
2
|
+
readonly callbackMissingToken: "callback_missing_token";
|
|
3
|
+
readonly callbackInvalidToken: "callback_invalid_token";
|
|
4
|
+
readonly callbackValidationFailed: "callback_validation_failed";
|
|
5
|
+
readonly unauthorized: "unauthorized";
|
|
6
|
+
readonly networkError: "network_error";
|
|
7
|
+
readonly apiError: "api_error";
|
|
8
|
+
readonly broadcastChannelUnsupported: "broadcast_channel_unsupported";
|
|
9
|
+
readonly serverOriginResolutionFailed: "server_origin_resolution_failed";
|
|
10
|
+
};
|
|
11
|
+
type AuthErrorCode = (typeof authErrorCode)[keyof typeof authErrorCode];
|
|
12
|
+
|
|
13
|
+
interface AuthSdkErrorPayload {
|
|
14
|
+
code: AuthErrorCode;
|
|
15
|
+
details: unknown;
|
|
16
|
+
message: string;
|
|
17
|
+
status: number | null;
|
|
18
|
+
}
|
|
19
|
+
declare class AuthSdkError extends Error {
|
|
20
|
+
readonly code: AuthErrorCode;
|
|
21
|
+
readonly details: unknown;
|
|
22
|
+
readonly status: number | null;
|
|
23
|
+
constructor(payload: AuthSdkErrorPayload);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { type AuthErrorCode as A, AuthSdkError as a, type AuthSdkErrorPayload as b, authErrorCode as c };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { A as AuthClient, G as GhostlySession } from './auth-client-CAHMjodm.js';
|
|
2
|
+
export { L as LoginOptions, P as ProcessCallbackResult, S as SessionListener, a as SessionRequestOptions } from './auth-client-CAHMjodm.js';
|
|
3
|
+
export { A as AuthErrorCode, a as AuthSdkError, b as AuthSdkErrorPayload, c as authErrorCode } from './auth-sdk-error-DKM7PyKC.js';
|
|
4
|
+
|
|
5
|
+
declare const authEndpoints: {
|
|
6
|
+
readonly loginStart: "/v1/auth/keycloak/login";
|
|
7
|
+
readonly validateKeycloakToken: "/v1/auth/keycloak/validate";
|
|
8
|
+
readonly session: "/v1/auth/me";
|
|
9
|
+
readonly logout: "/v1/auth/logout";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
declare const authQueryKeys: {
|
|
13
|
+
readonly token: "token";
|
|
14
|
+
};
|
|
15
|
+
declare const authRoutes: {
|
|
16
|
+
readonly root: "/";
|
|
17
|
+
readonly callback: "/auth/callback";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
declare function createAuthClient(): AuthClient;
|
|
21
|
+
|
|
22
|
+
interface KeycloakValidateRequest {
|
|
23
|
+
token: string;
|
|
24
|
+
}
|
|
25
|
+
interface KeycloakValidateResponse {
|
|
26
|
+
session: GhostlySession;
|
|
27
|
+
}
|
|
28
|
+
interface AuthErrorPayload {
|
|
29
|
+
code: string;
|
|
30
|
+
details: unknown;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { AuthClient, type AuthErrorPayload, GhostlySession, type KeycloakValidateRequest, type KeycloakValidateResponse, authEndpoints, authQueryKeys, authRoutes, createAuthClient };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
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/auth-keys.ts
|
|
11
|
+
var authQueryKeys = {
|
|
12
|
+
token: "token"
|
|
13
|
+
};
|
|
14
|
+
var authStorageKeys = {
|
|
15
|
+
returnTo: "ghostly-auth:return-to"
|
|
16
|
+
};
|
|
17
|
+
var authBroadcast = {
|
|
18
|
+
channelName: "ghostly-auth-channel",
|
|
19
|
+
sessionUpdatedEvent: "session-updated"
|
|
20
|
+
};
|
|
21
|
+
var authRoutes = {
|
|
22
|
+
root: "/",
|
|
23
|
+
callback: "/auth/callback"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/constants/http-status.ts
|
|
27
|
+
var httpStatus = {
|
|
28
|
+
ok: 200,
|
|
29
|
+
noContent: 204,
|
|
30
|
+
badRequest: 400,
|
|
31
|
+
unauthorized: 401
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// src/errors/auth-sdk-error.ts
|
|
35
|
+
var AuthSdkError = class extends Error {
|
|
36
|
+
code;
|
|
37
|
+
details;
|
|
38
|
+
status;
|
|
39
|
+
constructor(payload) {
|
|
40
|
+
super(payload.message);
|
|
41
|
+
this.name = "AuthSdkError";
|
|
42
|
+
this.code = payload.code;
|
|
43
|
+
this.details = payload.details;
|
|
44
|
+
this.status = payload.status;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/types/auth-error-code.ts
|
|
49
|
+
var authErrorCode = {
|
|
50
|
+
callbackMissingToken: "callback_missing_token",
|
|
51
|
+
callbackInvalidToken: "callback_invalid_token",
|
|
52
|
+
callbackValidationFailed: "callback_validation_failed",
|
|
53
|
+
unauthorized: "unauthorized",
|
|
54
|
+
networkError: "network_error",
|
|
55
|
+
apiError: "api_error",
|
|
56
|
+
broadcastChannelUnsupported: "broadcast_channel_unsupported",
|
|
57
|
+
serverOriginResolutionFailed: "server_origin_resolution_failed"
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/core/object-guards.ts
|
|
61
|
+
function isObjectRecord(value) {
|
|
62
|
+
return typeof value === "object" && value !== null;
|
|
63
|
+
}
|
|
64
|
+
function isStringValue(value) {
|
|
65
|
+
return typeof value === "string";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/core/session-parser.ts
|
|
69
|
+
function isStringArray(value) {
|
|
70
|
+
return Array.isArray(value) && value.every((entry) => isStringValue(entry));
|
|
71
|
+
}
|
|
72
|
+
function isGhostlySession(value) {
|
|
73
|
+
if (!isObjectRecord(value)) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
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);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/core/broadcast-sync.ts
|
|
80
|
+
function isSessionUpdatedMessage(value) {
|
|
81
|
+
if (!isObjectRecord(value)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (!isStringValue(value.type)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (value.type !== authBroadcast.sessionUpdatedEvent) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return value.session === null || isGhostlySession(value.session);
|
|
91
|
+
}
|
|
92
|
+
function createUnsupportedBroadcastChannelError() {
|
|
93
|
+
return new AuthSdkError({
|
|
94
|
+
code: authErrorCode.broadcastChannelUnsupported,
|
|
95
|
+
details: null,
|
|
96
|
+
message: "BroadcastChannel is unavailable in this runtime.",
|
|
97
|
+
status: null
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function createBroadcastSync(options) {
|
|
101
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
102
|
+
throw createUnsupportedBroadcastChannelError();
|
|
103
|
+
}
|
|
104
|
+
const channel = new BroadcastChannel(authBroadcast.channelName);
|
|
105
|
+
const onMessage = (event) => {
|
|
106
|
+
const messageEvent = event;
|
|
107
|
+
if (!isSessionUpdatedMessage(messageEvent.data)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
options.onSessionUpdated(messageEvent.data.session);
|
|
111
|
+
};
|
|
112
|
+
channel.addEventListener("message", onMessage);
|
|
113
|
+
return {
|
|
114
|
+
close() {
|
|
115
|
+
channel.removeEventListener("message", onMessage);
|
|
116
|
+
channel.close();
|
|
117
|
+
},
|
|
118
|
+
publishSession(session) {
|
|
119
|
+
const payload = {
|
|
120
|
+
session,
|
|
121
|
+
type: authBroadcast.sessionUpdatedEvent
|
|
122
|
+
};
|
|
123
|
+
channel.postMessage(payload);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/core/callback-url.ts
|
|
129
|
+
function readCallbackToken(url) {
|
|
130
|
+
return url.searchParams.get(authQueryKeys.token);
|
|
131
|
+
}
|
|
132
|
+
function removeCallbackToken(url) {
|
|
133
|
+
const nextUrl = new URL(url.toString());
|
|
134
|
+
nextUrl.searchParams.delete(authQueryKeys.token);
|
|
135
|
+
return nextUrl;
|
|
136
|
+
}
|
|
137
|
+
function replaceBrowserHistory(url) {
|
|
138
|
+
window.history.replaceState(null, "", url.toString());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/core/http-client.ts
|
|
142
|
+
var jsonContentType = "application/json";
|
|
143
|
+
var jsonHeaderName = "content-type";
|
|
144
|
+
var includeCredentials = "include";
|
|
145
|
+
var noStoreCache = "no-store";
|
|
146
|
+
function toTypedValue(value) {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
function mapHttpStatusToAuthErrorCode(status) {
|
|
150
|
+
if (status === httpStatus.unauthorized) {
|
|
151
|
+
return authErrorCode.unauthorized;
|
|
152
|
+
}
|
|
153
|
+
return authErrorCode.apiError;
|
|
154
|
+
}
|
|
155
|
+
async function parseJsonPayload(response) {
|
|
156
|
+
try {
|
|
157
|
+
return await response.json();
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function parseErrorPayload(response) {
|
|
163
|
+
const payload = await parseJsonPayload(response);
|
|
164
|
+
if (!isObjectRecord(payload)) {
|
|
165
|
+
return {
|
|
166
|
+
code: null,
|
|
167
|
+
details: null,
|
|
168
|
+
message: null
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const maybeCode = payload.code;
|
|
172
|
+
const maybeMessage = payload.message;
|
|
173
|
+
return {
|
|
174
|
+
code: isStringValue(maybeCode) ? maybeCode : null,
|
|
175
|
+
details: "details" in payload ? payload.details : null,
|
|
176
|
+
message: isStringValue(maybeMessage) ? maybeMessage : null
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function buildApiErrorMessage(method, path) {
|
|
180
|
+
return `Auth API request failed: ${method} ${path}`;
|
|
181
|
+
}
|
|
182
|
+
function buildNetworkErrorMessage(method, path) {
|
|
183
|
+
return `Auth API network failure: ${method} ${path}`;
|
|
184
|
+
}
|
|
185
|
+
async function request(options) {
|
|
186
|
+
const expectedStatus = options.expectedStatus ?? httpStatus.ok;
|
|
187
|
+
const headers = new Headers();
|
|
188
|
+
const hasBody = typeof options.body !== "undefined";
|
|
189
|
+
if (hasBody) {
|
|
190
|
+
headers.set(jsonHeaderName, jsonContentType);
|
|
191
|
+
}
|
|
192
|
+
const requestInit = {
|
|
193
|
+
cache: noStoreCache,
|
|
194
|
+
credentials: includeCredentials,
|
|
195
|
+
headers,
|
|
196
|
+
method: options.method
|
|
197
|
+
};
|
|
198
|
+
if (hasBody) {
|
|
199
|
+
requestInit.body = JSON.stringify(options.body);
|
|
200
|
+
}
|
|
201
|
+
let response;
|
|
202
|
+
try {
|
|
203
|
+
response = await fetch(options.path, requestInit);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw new AuthSdkError({
|
|
206
|
+
code: authErrorCode.networkError,
|
|
207
|
+
details: error,
|
|
208
|
+
message: buildNetworkErrorMessage(options.method, options.path),
|
|
209
|
+
status: null
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (response.status !== expectedStatus) {
|
|
213
|
+
const parsed = await parseErrorPayload(response);
|
|
214
|
+
throw new AuthSdkError({
|
|
215
|
+
code: mapHttpStatusToAuthErrorCode(response.status),
|
|
216
|
+
details: {
|
|
217
|
+
apiCode: parsed.code,
|
|
218
|
+
apiDetails: parsed.details
|
|
219
|
+
},
|
|
220
|
+
message: parsed.message ?? buildApiErrorMessage(options.method, options.path),
|
|
221
|
+
status: response.status
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (response.status === httpStatus.noContent) {
|
|
225
|
+
return toTypedValue(null);
|
|
226
|
+
}
|
|
227
|
+
const payload = await parseJsonPayload(response);
|
|
228
|
+
return toTypedValue(payload);
|
|
229
|
+
}
|
|
230
|
+
function getJson(path) {
|
|
231
|
+
return request({
|
|
232
|
+
method: "GET",
|
|
233
|
+
path
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function postJson(path, body) {
|
|
237
|
+
return request({
|
|
238
|
+
body,
|
|
239
|
+
method: "POST",
|
|
240
|
+
path
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function postEmpty(path) {
|
|
244
|
+
return request({
|
|
245
|
+
expectedStatus: httpStatus.noContent,
|
|
246
|
+
method: "POST",
|
|
247
|
+
path
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/core/runtime.ts
|
|
252
|
+
var browserRuntimeErrorMessage = "Browser runtime is required for this auth operation.";
|
|
253
|
+
function isBrowserRuntime() {
|
|
254
|
+
return typeof window !== "undefined";
|
|
255
|
+
}
|
|
256
|
+
function assertBrowserRuntime() {
|
|
257
|
+
if (isBrowserRuntime()) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
throw new AuthSdkError({
|
|
261
|
+
code: authErrorCode.apiError,
|
|
262
|
+
details: null,
|
|
263
|
+
message: browserRuntimeErrorMessage,
|
|
264
|
+
status: null
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/core/return-to-storage.ts
|
|
269
|
+
function sanitizeReturnTo(value) {
|
|
270
|
+
if (!value) {
|
|
271
|
+
return authRoutes.root;
|
|
272
|
+
}
|
|
273
|
+
if (!value.startsWith(authRoutes.root)) {
|
|
274
|
+
return authRoutes.root;
|
|
275
|
+
}
|
|
276
|
+
const protocolRelativePrefix = "//";
|
|
277
|
+
if (value.startsWith(protocolRelativePrefix)) {
|
|
278
|
+
return authRoutes.root;
|
|
279
|
+
}
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
function getCurrentBrowserPath() {
|
|
283
|
+
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
284
|
+
}
|
|
285
|
+
function saveReturnToPath(returnTo) {
|
|
286
|
+
assertBrowserRuntime();
|
|
287
|
+
const fallbackPath = getCurrentBrowserPath();
|
|
288
|
+
const sanitized = sanitizeReturnTo(returnTo ?? fallbackPath);
|
|
289
|
+
window.sessionStorage.setItem(authStorageKeys.returnTo, sanitized);
|
|
290
|
+
return sanitized;
|
|
291
|
+
}
|
|
292
|
+
function consumeReturnToPath() {
|
|
293
|
+
assertBrowserRuntime();
|
|
294
|
+
const value = window.sessionStorage.getItem(authStorageKeys.returnTo);
|
|
295
|
+
window.sessionStorage.removeItem(authStorageKeys.returnTo);
|
|
296
|
+
return sanitizeReturnTo(value);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/core/session-store.ts
|
|
300
|
+
var SessionStore = class {
|
|
301
|
+
listeners = /* @__PURE__ */ new Set();
|
|
302
|
+
resolvedSession = null;
|
|
303
|
+
resolveState = "pending";
|
|
304
|
+
getSessionIfResolved() {
|
|
305
|
+
if (this.resolveState === "pending") {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
return this.resolvedSession;
|
|
309
|
+
}
|
|
310
|
+
hasResolvedSession() {
|
|
311
|
+
return this.resolveState === "resolved";
|
|
312
|
+
}
|
|
313
|
+
setSession(session) {
|
|
314
|
+
this.resolveState = "resolved";
|
|
315
|
+
this.resolvedSession = session;
|
|
316
|
+
for (const listener of this.listeners) {
|
|
317
|
+
listener(session);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
subscribe(listener) {
|
|
321
|
+
this.listeners.add(listener);
|
|
322
|
+
return () => {
|
|
323
|
+
this.listeners.delete(listener);
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// src/core/auth-client.ts
|
|
329
|
+
function createPendingRedirectPromise() {
|
|
330
|
+
return new Promise(() => {
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function createInvalidSessionPayloadError(path) {
|
|
334
|
+
return new AuthSdkError({
|
|
335
|
+
code: authErrorCode.apiError,
|
|
336
|
+
details: null,
|
|
337
|
+
message: `Auth API response has invalid session shape: ${path}`,
|
|
338
|
+
status: null
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function toValidatedSession(payload, path) {
|
|
342
|
+
if (!isGhostlySession(payload)) {
|
|
343
|
+
throw createInvalidSessionPayloadError(path);
|
|
344
|
+
}
|
|
345
|
+
return payload;
|
|
346
|
+
}
|
|
347
|
+
function toCallbackFailure(error) {
|
|
348
|
+
if (error instanceof AuthSdkError) {
|
|
349
|
+
if (error.status === httpStatus.unauthorized) {
|
|
350
|
+
return new AuthSdkError({
|
|
351
|
+
code: authErrorCode.callbackInvalidToken,
|
|
352
|
+
details: error.details,
|
|
353
|
+
message: "Callback JWT is invalid or expired.",
|
|
354
|
+
status: error.status
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return new AuthSdkError({
|
|
358
|
+
code: authErrorCode.callbackValidationFailed,
|
|
359
|
+
details: error.details,
|
|
360
|
+
message: "Keycloak callback validation failed.",
|
|
361
|
+
status: error.status
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return new AuthSdkError({
|
|
365
|
+
code: authErrorCode.callbackValidationFailed,
|
|
366
|
+
details: error,
|
|
367
|
+
message: "Keycloak callback validation failed.",
|
|
368
|
+
status: null
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function createNoopBroadcastSync() {
|
|
372
|
+
return {
|
|
373
|
+
close() {
|
|
374
|
+
},
|
|
375
|
+
publishSession() {
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function createSafeBroadcastSync(onSessionUpdated) {
|
|
380
|
+
try {
|
|
381
|
+
return createBroadcastSync({
|
|
382
|
+
onSessionUpdated
|
|
383
|
+
});
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (error instanceof AuthSdkError && error.code === authErrorCode.broadcastChannelUnsupported) {
|
|
386
|
+
return createNoopBroadcastSync();
|
|
387
|
+
}
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
async function fetchCurrentSessionFromApi() {
|
|
392
|
+
const payload = await getJson(authEndpoints.session);
|
|
393
|
+
if (payload === null) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
return toValidatedSession(payload, authEndpoints.session);
|
|
397
|
+
}
|
|
398
|
+
function createAuthClient() {
|
|
399
|
+
assertBrowserRuntime();
|
|
400
|
+
const sessionStore = new SessionStore();
|
|
401
|
+
const broadcastSync = createSafeBroadcastSync((session) => {
|
|
402
|
+
sessionStore.setSession(session);
|
|
403
|
+
});
|
|
404
|
+
const getSession = async (options) => {
|
|
405
|
+
const forceRefresh = options?.forceRefresh ?? false;
|
|
406
|
+
if (sessionStore.hasResolvedSession() && !forceRefresh) {
|
|
407
|
+
return sessionStore.getSessionIfResolved();
|
|
408
|
+
}
|
|
409
|
+
const session = await fetchCurrentSessionFromApi();
|
|
410
|
+
sessionStore.setSession(session);
|
|
411
|
+
broadcastSync.publishSession(session);
|
|
412
|
+
return session;
|
|
413
|
+
};
|
|
414
|
+
const requireSession = async () => {
|
|
415
|
+
const session = await getSession();
|
|
416
|
+
if (session) {
|
|
417
|
+
return session;
|
|
418
|
+
}
|
|
419
|
+
throw new AuthSdkError({
|
|
420
|
+
code: authErrorCode.unauthorized,
|
|
421
|
+
details: null,
|
|
422
|
+
message: "Authenticated session is required.",
|
|
423
|
+
status: httpStatus.unauthorized
|
|
424
|
+
});
|
|
425
|
+
};
|
|
426
|
+
const login = (options) => {
|
|
427
|
+
saveReturnToPath(options?.returnTo);
|
|
428
|
+
window.location.assign(authEndpoints.loginStart);
|
|
429
|
+
};
|
|
430
|
+
const processCallback = async () => {
|
|
431
|
+
const currentUrl = new URL(window.location.href);
|
|
432
|
+
const token = readCallbackToken(currentUrl);
|
|
433
|
+
if (!token) {
|
|
434
|
+
throw new AuthSdkError({
|
|
435
|
+
code: authErrorCode.callbackMissingToken,
|
|
436
|
+
details: null,
|
|
437
|
+
message: "Missing callback token query parameter.",
|
|
438
|
+
status: httpStatus.badRequest
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
const cleanedUrl = removeCallbackToken(currentUrl);
|
|
442
|
+
replaceBrowserHistory(cleanedUrl);
|
|
443
|
+
try {
|
|
444
|
+
const payload = await postJson(
|
|
445
|
+
authEndpoints.validateKeycloakToken,
|
|
446
|
+
{ token }
|
|
447
|
+
);
|
|
448
|
+
const session = toValidatedSession(payload.session, authEndpoints.validateKeycloakToken);
|
|
449
|
+
sessionStore.setSession(session);
|
|
450
|
+
broadcastSync.publishSession(session);
|
|
451
|
+
return {
|
|
452
|
+
redirectTo: consumeReturnToPath(),
|
|
453
|
+
session
|
|
454
|
+
};
|
|
455
|
+
} catch (error) {
|
|
456
|
+
throw toCallbackFailure(error);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const completeCallbackRedirect = async () => {
|
|
460
|
+
const result = await processCallback();
|
|
461
|
+
window.location.replace(result.redirectTo);
|
|
462
|
+
return createPendingRedirectPromise();
|
|
463
|
+
};
|
|
464
|
+
const logout = async () => {
|
|
465
|
+
await postEmpty(authEndpoints.logout);
|
|
466
|
+
sessionStore.setSession(null);
|
|
467
|
+
broadcastSync.publishSession(null);
|
|
468
|
+
};
|
|
469
|
+
const subscribe = sessionStore.subscribe.bind(sessionStore);
|
|
470
|
+
return {
|
|
471
|
+
completeCallbackRedirect,
|
|
472
|
+
getSession,
|
|
473
|
+
login,
|
|
474
|
+
logout,
|
|
475
|
+
processCallback,
|
|
476
|
+
requireSession,
|
|
477
|
+
subscribe
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export { AuthSdkError, authEndpoints, authErrorCode, authQueryKeys, authRoutes, createAuthClient };
|
|
482
|
+
//# sourceMappingURL=index.js.map
|
|
483
|
+
//# sourceMappingURL=index.js.map
|