@ghostly-solutions/auth 0.1.1 → 0.2.2
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/LICENSE +13 -0
- package/README.md +200 -71
- package/dist/{auth-client-CAHMjodm.d.ts → auth-client-Cdkp07ii.d.ts} +14 -6
- package/dist/{auth-sdk-error-DKM7PyKC.d.ts → auth-sdk-error-D3gsfK9d.d.ts} +0 -3
- package/dist/extension.d.ts +49 -0
- package/dist/extension.js +634 -0
- package/dist/extension.js.map +1 -0
- package/dist/index.d.ts +13 -19
- package/dist/index.js +106 -119
- package/dist/index.js.map +1 -1
- package/dist/next.d.ts +4 -27
- package/dist/next.js +125 -383
- package/dist/next.js.map +1 -1
- package/dist/react.d.ts +4 -20
- package/dist/react.js +129 -176
- package/dist/react.js.map +1 -1
- package/docs/api-reference.md +66 -89
- package/docs/architecture.md +28 -46
- package/docs/development-and-ci.md +15 -19
- package/docs/index.md +1 -15
- package/docs/integration-guide.md +54 -80
- package/docs/overview.md +27 -28
- package/package.json +15 -2
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
// src/constants/auth-endpoints.ts
|
|
2
|
+
var authApiPrefix = "/oauth";
|
|
3
|
+
var authEndpoints = {
|
|
4
|
+
authorize: `${authApiPrefix}/authorize`,
|
|
5
|
+
extensionToken: `${authApiPrefix}/extension/token`,
|
|
6
|
+
session: `${authApiPrefix}/session`,
|
|
7
|
+
refresh: `${authApiPrefix}/refresh`,
|
|
8
|
+
logout: `${authApiPrefix}/logout`
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/constants/http-status.ts
|
|
12
|
+
var httpStatus = {
|
|
13
|
+
ok: 200,
|
|
14
|
+
noContent: 204,
|
|
15
|
+
unauthorized: 401
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/errors/auth-sdk-error.ts
|
|
19
|
+
var AuthSdkError = class extends Error {
|
|
20
|
+
code;
|
|
21
|
+
details;
|
|
22
|
+
status;
|
|
23
|
+
constructor(payload) {
|
|
24
|
+
super(payload.message);
|
|
25
|
+
this.name = "AuthSdkError";
|
|
26
|
+
this.code = payload.code;
|
|
27
|
+
this.details = payload.details;
|
|
28
|
+
this.status = payload.status;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/types/auth-error-code.ts
|
|
33
|
+
var authErrorCode = {
|
|
34
|
+
unauthorized: "unauthorized",
|
|
35
|
+
networkError: "network_error",
|
|
36
|
+
apiError: "api_error",
|
|
37
|
+
broadcastChannelUnsupported: "broadcast_channel_unsupported"};
|
|
38
|
+
|
|
39
|
+
// src/core/api-origin.ts
|
|
40
|
+
var slash = "/";
|
|
41
|
+
function normalizeApiOrigin(apiOrigin) {
|
|
42
|
+
const trimmed = apiOrigin.trim();
|
|
43
|
+
if (!trimmed) {
|
|
44
|
+
throw new AuthSdkError({
|
|
45
|
+
code: authErrorCode.apiError,
|
|
46
|
+
details: null,
|
|
47
|
+
message: "Auth API origin must be a non-empty absolute URL.",
|
|
48
|
+
status: null
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = new URL(trimmed);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new AuthSdkError({
|
|
56
|
+
code: authErrorCode.apiError,
|
|
57
|
+
details: error,
|
|
58
|
+
message: "Auth API origin must be a valid absolute URL.",
|
|
59
|
+
status: null
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (parsed.pathname !== slash || parsed.search || parsed.hash) {
|
|
63
|
+
throw new AuthSdkError({
|
|
64
|
+
code: authErrorCode.apiError,
|
|
65
|
+
details: null,
|
|
66
|
+
message: "Auth API origin must not include path, query, or hash.",
|
|
67
|
+
status: null
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return parsed.origin;
|
|
71
|
+
}
|
|
72
|
+
function resolveApiEndpoint(path, apiOrigin) {
|
|
73
|
+
if (!apiOrigin) {
|
|
74
|
+
return path;
|
|
75
|
+
}
|
|
76
|
+
return `${normalizeApiOrigin(apiOrigin)}${path}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/constants/auth-keys.ts
|
|
80
|
+
var authBroadcast = {
|
|
81
|
+
channelName: "ghostly-auth-channel",
|
|
82
|
+
sessionUpdatedEvent: "session-updated"
|
|
83
|
+
};
|
|
84
|
+
var authRoutes = {
|
|
85
|
+
root: "/"
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/core/object-guards.ts
|
|
89
|
+
function isObjectRecord(value) {
|
|
90
|
+
return typeof value === "object" && value !== null;
|
|
91
|
+
}
|
|
92
|
+
function isStringValue(value) {
|
|
93
|
+
return typeof value === "string";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/core/session-parser.ts
|
|
97
|
+
function isStringArray(value) {
|
|
98
|
+
return Array.isArray(value) && value.every((entry) => isStringValue(entry));
|
|
99
|
+
}
|
|
100
|
+
function isGhostlySession(value) {
|
|
101
|
+
if (!isObjectRecord(value)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
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);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/core/broadcast-sync.ts
|
|
108
|
+
function isSessionUpdatedMessage(value) {
|
|
109
|
+
if (!isObjectRecord(value)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
if (!isStringValue(value.type)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (value.type !== authBroadcast.sessionUpdatedEvent) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return value.session === null || isGhostlySession(value.session);
|
|
119
|
+
}
|
|
120
|
+
function createUnsupportedBroadcastChannelError() {
|
|
121
|
+
return new AuthSdkError({
|
|
122
|
+
code: authErrorCode.broadcastChannelUnsupported,
|
|
123
|
+
details: null,
|
|
124
|
+
message: "BroadcastChannel is unavailable in this runtime.",
|
|
125
|
+
status: null
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function createBroadcastSync(options) {
|
|
129
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
130
|
+
throw createUnsupportedBroadcastChannelError();
|
|
131
|
+
}
|
|
132
|
+
const channel = new BroadcastChannel(authBroadcast.channelName);
|
|
133
|
+
const onMessage = (event) => {
|
|
134
|
+
const messageEvent = event;
|
|
135
|
+
if (!isSessionUpdatedMessage(messageEvent.data)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
options.onSessionUpdated(messageEvent.data.session);
|
|
139
|
+
};
|
|
140
|
+
channel.addEventListener("message", onMessage);
|
|
141
|
+
return {
|
|
142
|
+
close() {
|
|
143
|
+
channel.removeEventListener("message", onMessage);
|
|
144
|
+
channel.close();
|
|
145
|
+
},
|
|
146
|
+
publishSession(session) {
|
|
147
|
+
const payload = {
|
|
148
|
+
session,
|
|
149
|
+
type: authBroadcast.sessionUpdatedEvent
|
|
150
|
+
};
|
|
151
|
+
channel.postMessage(payload);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/core/runtime.ts
|
|
157
|
+
var browserRuntimeErrorMessage = "Browser runtime is required for this auth operation.";
|
|
158
|
+
function isBrowserRuntime() {
|
|
159
|
+
return typeof window !== "undefined";
|
|
160
|
+
}
|
|
161
|
+
function assertBrowserRuntime() {
|
|
162
|
+
if (isBrowserRuntime()) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
throw new AuthSdkError({
|
|
166
|
+
code: authErrorCode.apiError,
|
|
167
|
+
details: null,
|
|
168
|
+
message: browserRuntimeErrorMessage,
|
|
169
|
+
status: null
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/core/return-to-storage.ts
|
|
174
|
+
function sanitizeReturnTo(value) {
|
|
175
|
+
if (!value) {
|
|
176
|
+
return authRoutes.root;
|
|
177
|
+
}
|
|
178
|
+
if (!value.startsWith(authRoutes.root)) {
|
|
179
|
+
return authRoutes.root;
|
|
180
|
+
}
|
|
181
|
+
const protocolRelativePrefix = "//";
|
|
182
|
+
if (value.startsWith(protocolRelativePrefix)) {
|
|
183
|
+
return authRoutes.root;
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
function getCurrentBrowserPath() {
|
|
188
|
+
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
189
|
+
}
|
|
190
|
+
function resolveReturnToPath(returnTo) {
|
|
191
|
+
assertBrowserRuntime();
|
|
192
|
+
const fallbackPath = getCurrentBrowserPath();
|
|
193
|
+
return sanitizeReturnTo(returnTo ?? fallbackPath);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/core/session-store.ts
|
|
197
|
+
var SessionStore = class {
|
|
198
|
+
listeners = /* @__PURE__ */ new Set();
|
|
199
|
+
resolvedSession = null;
|
|
200
|
+
resolveState = "pending";
|
|
201
|
+
getSessionIfResolved() {
|
|
202
|
+
if (this.resolveState === "pending") {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return this.resolvedSession;
|
|
206
|
+
}
|
|
207
|
+
hasResolvedSession() {
|
|
208
|
+
return this.resolveState === "resolved";
|
|
209
|
+
}
|
|
210
|
+
setSession(session) {
|
|
211
|
+
this.resolveState = "resolved";
|
|
212
|
+
this.resolvedSession = session;
|
|
213
|
+
for (const listener of this.listeners) {
|
|
214
|
+
listener(session);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
subscribe(listener) {
|
|
218
|
+
this.listeners.add(listener);
|
|
219
|
+
return () => {
|
|
220
|
+
this.listeners.delete(listener);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// src/adapters/extension/auth-client.ts
|
|
226
|
+
var extensionSessionHeader = "X-Ghostly-Session-Id";
|
|
227
|
+
var includeCredentials = "include";
|
|
228
|
+
var noStoreCache = "no-store";
|
|
229
|
+
var tokenFreshnessLeewayMs = 6e4;
|
|
230
|
+
function createNoopBroadcastSync() {
|
|
231
|
+
return {
|
|
232
|
+
close() {
|
|
233
|
+
},
|
|
234
|
+
publishSession() {
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function createSafeBroadcastSync(onSessionUpdated) {
|
|
239
|
+
try {
|
|
240
|
+
return createBroadcastSync({
|
|
241
|
+
onSessionUpdated
|
|
242
|
+
});
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (error instanceof AuthSdkError && error.code === authErrorCode.broadcastChannelUnsupported) {
|
|
245
|
+
return createNoopBroadcastSync();
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function buildApiErrorMessage(method, path) {
|
|
251
|
+
return `Auth API request failed: ${method} ${path}`;
|
|
252
|
+
}
|
|
253
|
+
function buildNetworkErrorMessage(method, path) {
|
|
254
|
+
return `Auth API network failure: ${method} ${path}`;
|
|
255
|
+
}
|
|
256
|
+
function createInvalidSessionPayloadError(path) {
|
|
257
|
+
return new AuthSdkError({
|
|
258
|
+
code: authErrorCode.apiError,
|
|
259
|
+
details: null,
|
|
260
|
+
message: `Auth API response has invalid session shape: ${path}`,
|
|
261
|
+
status: null
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function toInitResult(session) {
|
|
265
|
+
return {
|
|
266
|
+
session,
|
|
267
|
+
status: session ? "authenticated" : "unauthenticated"
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function parseSessionPayload(payload, path) {
|
|
271
|
+
if (payload === null) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
if (!isGhostlySession(payload)) {
|
|
275
|
+
throw createInvalidSessionPayloadError(path);
|
|
276
|
+
}
|
|
277
|
+
return payload;
|
|
278
|
+
}
|
|
279
|
+
function isExtensionAccessToken(value) {
|
|
280
|
+
if (!isObjectRecord(value)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
return isStringValue(value.accessToken) && isStringValue(value.application) && (value.expiresAt === null || isStringValue(value.expiresAt)) && (value.session === null || isGhostlySession(value.session)) && isStringValue(value.tokenType);
|
|
284
|
+
}
|
|
285
|
+
function isStoredTokenFresh(token) {
|
|
286
|
+
if (!token || !token.accessToken.trim()) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
if (!token.expiresAt) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
const expiresAt = Date.parse(token.expiresAt);
|
|
293
|
+
if (Number.isNaN(expiresAt)) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
return expiresAt - Date.now() > tokenFreshnessLeewayMs;
|
|
297
|
+
}
|
|
298
|
+
function buildAuthorizeUrl(apiOrigin, returnTo, application) {
|
|
299
|
+
const authorizeEndpoint = resolveApiEndpoint(authEndpoints.authorize, apiOrigin);
|
|
300
|
+
const authorizeUrl = new URL(authorizeEndpoint, window.location.origin);
|
|
301
|
+
authorizeUrl.searchParams.set("return_to", returnTo);
|
|
302
|
+
if (application?.trim()) {
|
|
303
|
+
authorizeUrl.searchParams.set("app", application.trim());
|
|
304
|
+
}
|
|
305
|
+
return authorizeUrl.toString();
|
|
306
|
+
}
|
|
307
|
+
function parseErrorPayload(payload) {
|
|
308
|
+
if (!isObjectRecord(payload)) {
|
|
309
|
+
return {
|
|
310
|
+
details: null,
|
|
311
|
+
message: null
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
details: "details" in payload ? payload.details : null,
|
|
316
|
+
message: isStringValue(payload.message) ? payload.message : isStringValue(payload.error) ? payload.error : null
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function createRequest(options) {
|
|
320
|
+
const resolveEndpoint = (path) => resolveApiEndpoint(path, options.apiOrigin);
|
|
321
|
+
return async function request(requestOptions) {
|
|
322
|
+
const expectedStatus = requestOptions.expectedStatus ?? httpStatus.ok;
|
|
323
|
+
const headers = new Headers();
|
|
324
|
+
const sessionId = (await options.resolveSessionId?.())?.trim() || "";
|
|
325
|
+
if (sessionId) {
|
|
326
|
+
headers.set(extensionSessionHeader, sessionId);
|
|
327
|
+
}
|
|
328
|
+
let response;
|
|
329
|
+
try {
|
|
330
|
+
response = await fetch(resolveEndpoint(requestOptions.path), {
|
|
331
|
+
cache: noStoreCache,
|
|
332
|
+
credentials: includeCredentials,
|
|
333
|
+
headers,
|
|
334
|
+
method: requestOptions.method
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
throw new AuthSdkError({
|
|
338
|
+
code: authErrorCode.networkError,
|
|
339
|
+
details: error,
|
|
340
|
+
message: buildNetworkErrorMessage(requestOptions.method, requestOptions.path),
|
|
341
|
+
status: null
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (response.status !== expectedStatus) {
|
|
345
|
+
let parsedPayload = null;
|
|
346
|
+
try {
|
|
347
|
+
parsedPayload = await response.json();
|
|
348
|
+
} catch {
|
|
349
|
+
parsedPayload = null;
|
|
350
|
+
}
|
|
351
|
+
const parsed = parseErrorPayload(parsedPayload);
|
|
352
|
+
throw new AuthSdkError({
|
|
353
|
+
code: response.status === httpStatus.unauthorized ? authErrorCode.unauthorized : authErrorCode.apiError,
|
|
354
|
+
details: parsed.details,
|
|
355
|
+
message: parsed.message ?? buildApiErrorMessage(requestOptions.method, requestOptions.path),
|
|
356
|
+
status: response.status
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (response.status === httpStatus.noContent) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
return await response.json();
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function createLoadSession(request) {
|
|
366
|
+
return async () => {
|
|
367
|
+
const payload = await request({
|
|
368
|
+
method: "GET",
|
|
369
|
+
path: authEndpoints.session
|
|
370
|
+
});
|
|
371
|
+
return parseSessionPayload(payload, authEndpoints.session);
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function createInit(sessionStore, loadSession, publishSession) {
|
|
375
|
+
let initPromise = null;
|
|
376
|
+
return async (initOptions) => {
|
|
377
|
+
const forceRefresh = initOptions?.forceRefresh ?? false;
|
|
378
|
+
if (sessionStore.hasResolvedSession() && !forceRefresh) {
|
|
379
|
+
return toInitResult(sessionStore.getSessionIfResolved());
|
|
380
|
+
}
|
|
381
|
+
if (initPromise) {
|
|
382
|
+
return initPromise;
|
|
383
|
+
}
|
|
384
|
+
initPromise = (async () => {
|
|
385
|
+
const session = await loadSession();
|
|
386
|
+
sessionStore.setSession(session);
|
|
387
|
+
publishSession(session);
|
|
388
|
+
return toInitResult(session);
|
|
389
|
+
})();
|
|
390
|
+
try {
|
|
391
|
+
return await initPromise;
|
|
392
|
+
} finally {
|
|
393
|
+
initPromise = null;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function createRefresh(request, sessionStore, publishSession) {
|
|
398
|
+
return async () => {
|
|
399
|
+
const payload = await request({
|
|
400
|
+
method: "POST",
|
|
401
|
+
path: authEndpoints.refresh
|
|
402
|
+
});
|
|
403
|
+
const session = parseSessionPayload(payload, authEndpoints.refresh);
|
|
404
|
+
sessionStore.setSession(session);
|
|
405
|
+
publishSession(session);
|
|
406
|
+
return session;
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function createGetAccessToken(request, sessionStore, publishSession, persistAccessToken, restoreAccessToken) {
|
|
410
|
+
return async (requestOptions) => {
|
|
411
|
+
const forceRefresh = requestOptions?.forceRefresh ?? false;
|
|
412
|
+
const stored = await restoreAccessToken?.() ?? null;
|
|
413
|
+
if (!forceRefresh && isStoredTokenFresh(stored)) {
|
|
414
|
+
if (stored.session) {
|
|
415
|
+
sessionStore.setSession(stored.session);
|
|
416
|
+
}
|
|
417
|
+
return stored;
|
|
418
|
+
}
|
|
419
|
+
const payload = await request({
|
|
420
|
+
method: "GET",
|
|
421
|
+
path: authEndpoints.extensionToken
|
|
422
|
+
});
|
|
423
|
+
if (!isExtensionAccessToken(payload)) {
|
|
424
|
+
throw new AuthSdkError({
|
|
425
|
+
code: authErrorCode.apiError,
|
|
426
|
+
details: null,
|
|
427
|
+
message: `Auth API response has invalid extension token shape: ${authEndpoints.extensionToken}`,
|
|
428
|
+
status: null
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const nextToken = {
|
|
432
|
+
accessToken: payload.accessToken,
|
|
433
|
+
application: payload.application,
|
|
434
|
+
expiresAt: payload.expiresAt,
|
|
435
|
+
session: payload.session,
|
|
436
|
+
tokenType: payload.tokenType
|
|
437
|
+
};
|
|
438
|
+
sessionStore.setSession(nextToken.session);
|
|
439
|
+
publishSession(nextToken.session);
|
|
440
|
+
await persistAccessToken?.(nextToken);
|
|
441
|
+
return nextToken;
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function createLogout(request, sessionStore, publishSession, options) {
|
|
445
|
+
return async () => {
|
|
446
|
+
await request({
|
|
447
|
+
expectedStatus: httpStatus.noContent,
|
|
448
|
+
method: "POST",
|
|
449
|
+
path: authEndpoints.logout
|
|
450
|
+
});
|
|
451
|
+
await options.clearSessionId?.();
|
|
452
|
+
sessionStore.setSession(null);
|
|
453
|
+
publishSession(null);
|
|
454
|
+
await options.persistAccessToken?.(null);
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function createExtensionAuthClient(options) {
|
|
458
|
+
const sessionStore = new SessionStore();
|
|
459
|
+
const broadcastSync = createSafeBroadcastSync((session) => {
|
|
460
|
+
sessionStore.setSession(session);
|
|
461
|
+
});
|
|
462
|
+
const publishSession = broadcastSync.publishSession.bind(broadcastSync);
|
|
463
|
+
const request = createRequest(options);
|
|
464
|
+
const loadSession = createLoadSession(request);
|
|
465
|
+
const init = createInit(sessionStore, loadSession, publishSession);
|
|
466
|
+
const refresh = createRefresh(request, sessionStore, publishSession);
|
|
467
|
+
const getAccessToken = createGetAccessToken(
|
|
468
|
+
request,
|
|
469
|
+
sessionStore,
|
|
470
|
+
publishSession,
|
|
471
|
+
options.persistAccessToken,
|
|
472
|
+
options.restoreAccessToken
|
|
473
|
+
);
|
|
474
|
+
const logout = createLogout(request, sessionStore, publishSession, options);
|
|
475
|
+
const getSession = async (requestOptions) => {
|
|
476
|
+
const result = await init({
|
|
477
|
+
forceRefresh: requestOptions?.forceRefresh ?? false
|
|
478
|
+
});
|
|
479
|
+
return result.session;
|
|
480
|
+
};
|
|
481
|
+
const requireSession = async () => {
|
|
482
|
+
const session = await getSession();
|
|
483
|
+
if (session) {
|
|
484
|
+
return session;
|
|
485
|
+
}
|
|
486
|
+
throw new AuthSdkError({
|
|
487
|
+
code: authErrorCode.unauthorized,
|
|
488
|
+
details: null,
|
|
489
|
+
message: "Authenticated session is required.",
|
|
490
|
+
status: httpStatus.unauthorized
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
const loginWithTabFlow = async (loginOptions) => {
|
|
494
|
+
if (!options.openAuthorizePage) {
|
|
495
|
+
throw new Error("Extension auth client requires openAuthorizePage for tab login flow.");
|
|
496
|
+
}
|
|
497
|
+
const returnTo = resolveReturnToPath(loginOptions?.returnTo);
|
|
498
|
+
const authorizeUrl = buildAuthorizeUrl(
|
|
499
|
+
options.apiOrigin,
|
|
500
|
+
returnTo,
|
|
501
|
+
loginOptions?.application ?? options.application
|
|
502
|
+
);
|
|
503
|
+
await options.openAuthorizePage({
|
|
504
|
+
authorizeUrl
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
const loginWithWebAuthFlow = async (loginOptions) => {
|
|
508
|
+
if (!options.launchWebAuthFlow) {
|
|
509
|
+
throw new Error("Extension auth client requires launchWebAuthFlow for web auth flow.");
|
|
510
|
+
}
|
|
511
|
+
const returnTo = resolveReturnToPath(loginOptions?.returnTo);
|
|
512
|
+
const authorizeUrl = buildAuthorizeUrl(
|
|
513
|
+
options.apiOrigin,
|
|
514
|
+
returnTo,
|
|
515
|
+
loginOptions?.application ?? options.application
|
|
516
|
+
);
|
|
517
|
+
await options.launchWebAuthFlow({
|
|
518
|
+
authorizeUrl
|
|
519
|
+
});
|
|
520
|
+
await refresh();
|
|
521
|
+
};
|
|
522
|
+
return {
|
|
523
|
+
init,
|
|
524
|
+
getSession,
|
|
525
|
+
login(loginOptions) {
|
|
526
|
+
if (options.openAuthorizePage) {
|
|
527
|
+
void loginWithTabFlow(loginOptions);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
void loginWithWebAuthFlow(loginOptions);
|
|
531
|
+
},
|
|
532
|
+
logout,
|
|
533
|
+
refresh,
|
|
534
|
+
requireSession,
|
|
535
|
+
subscribe(listener) {
|
|
536
|
+
return sessionStore.subscribe(listener);
|
|
537
|
+
},
|
|
538
|
+
getAccessToken,
|
|
539
|
+
loginWithTabFlow,
|
|
540
|
+
loginWithWebAuthFlow
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/adapters/extension/chrome-auth-client.ts
|
|
545
|
+
var defaultSessionCookieName = "gs_auth_session";
|
|
546
|
+
var defaultAccessTokenStorageKey = "authToken";
|
|
547
|
+
var defaultTokenExpiresAtStorageKey = "authTokenExpiresAt";
|
|
548
|
+
var defaultSessionStorageKey = "authSession";
|
|
549
|
+
function getChromeRuntime() {
|
|
550
|
+
return globalThis.chrome ?? {};
|
|
551
|
+
}
|
|
552
|
+
function createChromeExtensionAuthClient(options) {
|
|
553
|
+
const sessionCookieName = options.sessionCookieName?.trim() || defaultSessionCookieName;
|
|
554
|
+
const accessTokenStorageKey = options.accessTokenStorageKey?.trim() || defaultAccessTokenStorageKey;
|
|
555
|
+
const tokenExpiresAtStorageKey = options.tokenExpiresAtStorageKey?.trim() || defaultTokenExpiresAtStorageKey;
|
|
556
|
+
const sessionStorageKey = options.sessionStorageKey?.trim() || defaultSessionStorageKey;
|
|
557
|
+
const resolveSessionId = async () => {
|
|
558
|
+
const cookies = getChromeRuntime().cookies;
|
|
559
|
+
if (!cookies) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
const cookie = await cookies.get({
|
|
563
|
+
name: sessionCookieName,
|
|
564
|
+
url: `${options.apiOrigin}/`
|
|
565
|
+
});
|
|
566
|
+
return cookie?.value?.trim() || null;
|
|
567
|
+
};
|
|
568
|
+
const clearSessionId = async () => {
|
|
569
|
+
const cookies = getChromeRuntime().cookies;
|
|
570
|
+
if (!cookies) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
await cookies.remove({
|
|
574
|
+
name: sessionCookieName,
|
|
575
|
+
url: `${options.apiOrigin}/`
|
|
576
|
+
});
|
|
577
|
+
};
|
|
578
|
+
const persistAccessToken = async (token) => {
|
|
579
|
+
const storage = getChromeRuntime().storage?.local;
|
|
580
|
+
if (!storage) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (!token) {
|
|
584
|
+
await storage.remove([accessTokenStorageKey, tokenExpiresAtStorageKey, sessionStorageKey]);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
await storage.set({
|
|
588
|
+
[accessTokenStorageKey]: token.accessToken,
|
|
589
|
+
[tokenExpiresAtStorageKey]: token.expiresAt || null,
|
|
590
|
+
[sessionStorageKey]: token.session || null
|
|
591
|
+
});
|
|
592
|
+
};
|
|
593
|
+
const restoreAccessToken = async () => {
|
|
594
|
+
const storage = getChromeRuntime().storage?.local;
|
|
595
|
+
if (!storage) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
const stored = await storage.get([
|
|
599
|
+
accessTokenStorageKey,
|
|
600
|
+
tokenExpiresAtStorageKey,
|
|
601
|
+
sessionStorageKey
|
|
602
|
+
]);
|
|
603
|
+
const rawAccessToken = stored[accessTokenStorageKey];
|
|
604
|
+
if (typeof rawAccessToken !== "string" || rawAccessToken.trim() === "") {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
accessToken: rawAccessToken,
|
|
609
|
+
application: options.application?.trim() || "",
|
|
610
|
+
expiresAt: typeof stored[tokenExpiresAtStorageKey] === "string" ? stored[tokenExpiresAtStorageKey] : null,
|
|
611
|
+
session: stored[sessionStorageKey] ?? null,
|
|
612
|
+
tokenType: "Bearer"
|
|
613
|
+
};
|
|
614
|
+
};
|
|
615
|
+
const openAuthorizePage = async ({ authorizeUrl }) => {
|
|
616
|
+
const tabs = getChromeRuntime().tabs;
|
|
617
|
+
if (!tabs) {
|
|
618
|
+
throw new Error("Chrome tabs API is unavailable.");
|
|
619
|
+
}
|
|
620
|
+
await tabs.create({ active: true, url: authorizeUrl });
|
|
621
|
+
};
|
|
622
|
+
return createExtensionAuthClient({
|
|
623
|
+
...options,
|
|
624
|
+
clearSessionId,
|
|
625
|
+
openAuthorizePage,
|
|
626
|
+
persistAccessToken,
|
|
627
|
+
resolveSessionId,
|
|
628
|
+
restoreAccessToken
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export { createChromeExtensionAuthClient, createExtensionAuthClient };
|
|
633
|
+
//# sourceMappingURL=extension.js.map
|
|
634
|
+
//# sourceMappingURL=extension.js.map
|