@pinooxhq/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/LICENSE +21 -0
- package/README.md +705 -0
- package/dist/createAuth-LO46VVEW.d.cts +176 -0
- package/dist/createAuth-LO46VVEW.d.ts +176 -0
- package/dist/index.cjs +853 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +71 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +836 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +862 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +38 -0
- package/dist/react/index.d.ts +38 -0
- package/dist/react/index.js +857 -0
- package/dist/react/index.js.map +1 -0
- package/dist/svelte/index.cjs +922 -0
- package/dist/svelte/index.cjs.map +1 -0
- package/dist/svelte/index.d.cts +60 -0
- package/dist/svelte/index.d.ts +60 -0
- package/dist/svelte/index.js +915 -0
- package/dist/svelte/index.js.map +1 -0
- package/dist/vue/index.cjs +877 -0
- package/dist/vue/index.cjs.map +1 -0
- package/dist/vue/index.d.cts +71 -0
- package/dist/vue/index.d.ts +71 -0
- package/dist/vue/index.js +871 -0
- package/dist/vue/index.js.map +1 -0
- package/package.json +97 -0
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
import { writable, derived } from 'svelte/store';
|
|
2
|
+
|
|
3
|
+
// src/svelte/useAuth.ts
|
|
4
|
+
|
|
5
|
+
// src/core/logger.ts
|
|
6
|
+
var defaultSink = (event, enabled) => {
|
|
7
|
+
if (!enabled && event.level === "debug") {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const prefix = `[pinoox-auth:${event.code}]`;
|
|
11
|
+
const payload = event.context ? [prefix, event.message, event.context] : [prefix, event.message];
|
|
12
|
+
switch (event.level) {
|
|
13
|
+
case "error":
|
|
14
|
+
console.error(...payload);
|
|
15
|
+
break;
|
|
16
|
+
case "warn":
|
|
17
|
+
console.warn(...payload);
|
|
18
|
+
break;
|
|
19
|
+
case "info":
|
|
20
|
+
console.info(...payload);
|
|
21
|
+
break;
|
|
22
|
+
default:
|
|
23
|
+
console.debug(...payload);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
function createLogger(debug = false, sink) {
|
|
27
|
+
let customSink = null;
|
|
28
|
+
const emit = (event) => {
|
|
29
|
+
if (customSink) {
|
|
30
|
+
customSink(event);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
defaultSink(event, debug);
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
emit,
|
|
37
|
+
debug: (code, message, context) => emit({ level: "debug", code, message, context }),
|
|
38
|
+
info: (code, message, context) => emit({ level: "info", code, message, context }),
|
|
39
|
+
warn: (code, message, context) => emit({ level: "warn", code, message, context }),
|
|
40
|
+
error: (code, message, context) => emit({ level: "error", code, message, context }),
|
|
41
|
+
setSink: (next) => {
|
|
42
|
+
customSink = next;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/core/returnPath.ts
|
|
48
|
+
var DEFAULT_FALLBACK = "/";
|
|
49
|
+
var DEFAULT_BLOCKED = ["/account"];
|
|
50
|
+
function isSafeReturnPath(value, blockedPrefixes = DEFAULT_BLOCKED) {
|
|
51
|
+
if (typeof value !== "string") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
if (!trimmed.startsWith("/") || trimmed.startsWith("//")) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (trimmed.includes("://") || trimmed.includes("\\")) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
for (const prefix of blockedPrefixes) {
|
|
62
|
+
if (!prefix) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (trimmed === prefix || trimmed.startsWith(`${prefix}/`)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
function resolveReturnPath(queryOrPath = void 0, fallback = DEFAULT_FALLBACK, blockedPrefixes = DEFAULT_BLOCKED) {
|
|
72
|
+
let candidate = queryOrPath;
|
|
73
|
+
if (queryOrPath && typeof queryOrPath === "object" && !Array.isArray(queryOrPath)) {
|
|
74
|
+
candidate = queryOrPath.redirect;
|
|
75
|
+
}
|
|
76
|
+
if (candidate === void 0 && typeof window !== "undefined") {
|
|
77
|
+
candidate = new URLSearchParams(window.location.search).get("redirect");
|
|
78
|
+
}
|
|
79
|
+
if (isSafeReturnPath(candidate, blockedPrefixes)) {
|
|
80
|
+
return candidate.trim();
|
|
81
|
+
}
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
function resolveSiteOrigin(bootUrl, explicit) {
|
|
85
|
+
const candidates = [explicit, bootUrl?.SITE];
|
|
86
|
+
for (const value of candidates) {
|
|
87
|
+
if (typeof value !== "string" || !value) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
return new URL(value).origin;
|
|
92
|
+
} catch {
|
|
93
|
+
const cleaned = value.replace(/\/$/, "");
|
|
94
|
+
if (/^https?:\/\//i.test(cleaned)) {
|
|
95
|
+
return cleaned;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
function toAbsoluteReturnUrl(path, siteOrigin) {
|
|
102
|
+
if (typeof path !== "string" || !path.startsWith("/")) {
|
|
103
|
+
return path;
|
|
104
|
+
}
|
|
105
|
+
if (!siteOrigin || typeof window === "undefined") {
|
|
106
|
+
return path;
|
|
107
|
+
}
|
|
108
|
+
if (window.location.origin === siteOrigin) {
|
|
109
|
+
return path;
|
|
110
|
+
}
|
|
111
|
+
return `${siteOrigin.replace(/\/$/, "")}${path}`;
|
|
112
|
+
}
|
|
113
|
+
function redirectToReturn(queryOrPath, options = {}) {
|
|
114
|
+
if (typeof window === "undefined") {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const path = resolveReturnPath(queryOrPath, options.fallback, options.blockedPrefixes);
|
|
118
|
+
window.location.href = toAbsoluteReturnUrl(path, options.siteOrigin);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/core/resolveConfig.ts
|
|
122
|
+
function readBoot() {
|
|
123
|
+
if (typeof globalThis === "undefined") {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
const g = globalThis;
|
|
127
|
+
if (g.__PINOOX__ && typeof g.__PINOOX__ === "object") {
|
|
128
|
+
return g.__PINOOX__;
|
|
129
|
+
}
|
|
130
|
+
if (g.PINOOX?.URL) {
|
|
131
|
+
return { url: g.PINOOX.URL };
|
|
132
|
+
}
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
function normalizeMode(value) {
|
|
136
|
+
const mode = String(value ?? "jwt").toLowerCase();
|
|
137
|
+
if (mode === "cookie" || mode === "session" || mode === "jwt") {
|
|
138
|
+
return mode;
|
|
139
|
+
}
|
|
140
|
+
return "jwt";
|
|
141
|
+
}
|
|
142
|
+
function normalizeBaseUrl(value) {
|
|
143
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
let base = value.trim().replace(/\/$/, "");
|
|
147
|
+
if (!/^https?:\/\//i.test(base) && !base.startsWith("/")) {
|
|
148
|
+
base = `/${base}`;
|
|
149
|
+
}
|
|
150
|
+
return base;
|
|
151
|
+
}
|
|
152
|
+
function joinUrl(base, path) {
|
|
153
|
+
if (!path) {
|
|
154
|
+
return base;
|
|
155
|
+
}
|
|
156
|
+
if (/^https?:\/\//i.test(path)) {
|
|
157
|
+
return path;
|
|
158
|
+
}
|
|
159
|
+
if (path.startsWith("/")) {
|
|
160
|
+
return path;
|
|
161
|
+
}
|
|
162
|
+
const normalizedBase = base.replace(/\/$/, "");
|
|
163
|
+
return `${normalizedBase}/${path.replace(/^\//, "")}`;
|
|
164
|
+
}
|
|
165
|
+
function resolveEndpoint(path, fallback, baseUrl) {
|
|
166
|
+
const value = typeof path === "string" && path.trim() !== "" ? path.trim() : fallback;
|
|
167
|
+
if (/^https?:\/\//i.test(value) || value.startsWith("/")) {
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
if (baseUrl) {
|
|
171
|
+
return joinUrl(baseUrl, value);
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
function inferStrategy(bootAuth, explicit) {
|
|
176
|
+
if (explicit) {
|
|
177
|
+
return normalizeStrategy(explicit) ?? explicit;
|
|
178
|
+
}
|
|
179
|
+
const fromBoot = normalizeStrategy(bootAuth?.strategy);
|
|
180
|
+
if (fromBoot) {
|
|
181
|
+
return fromBoot;
|
|
182
|
+
}
|
|
183
|
+
if (bootAuth?.source) {
|
|
184
|
+
return "remote";
|
|
185
|
+
}
|
|
186
|
+
return "local";
|
|
187
|
+
}
|
|
188
|
+
function normalizeStrategy(value) {
|
|
189
|
+
if (value === "local" || value === "provider") {
|
|
190
|
+
return "local";
|
|
191
|
+
}
|
|
192
|
+
if (value === "remote" || value === "consumer" || value === "external") {
|
|
193
|
+
return "remote";
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function defaultEndpoints(apiBase, strategy, baseUrl) {
|
|
198
|
+
if (strategy === "remote") {
|
|
199
|
+
if (baseUrl) {
|
|
200
|
+
return {
|
|
201
|
+
login: "auth/login",
|
|
202
|
+
logout: "auth/logout",
|
|
203
|
+
me: "auth/get"
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
login: "/account/api/v1/auth/login",
|
|
208
|
+
logout: "/account/api/v1/auth/logout",
|
|
209
|
+
me: "/account/api/v1/auth/get"
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
login: joinUrl(apiBase, "auth/login"),
|
|
214
|
+
logout: joinUrl(apiBase, "auth/logout"),
|
|
215
|
+
me: joinUrl(apiBase, "auth/get")
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function resolveConfig(options = {}, logger) {
|
|
219
|
+
const boot = options.boot ?? readBoot();
|
|
220
|
+
const bootAuth = boot.auth ?? {};
|
|
221
|
+
const apiBase = (options.apiBase ?? boot.url?.API ?? "").replace(/\/$/, "");
|
|
222
|
+
const strategy = inferStrategy(bootAuth, options.strategy);
|
|
223
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? bootAuth.baseUrl ?? null);
|
|
224
|
+
const defaults = defaultEndpoints(apiBase || "/", strategy, baseUrl);
|
|
225
|
+
const bootEndpoints = bootAuth.endpoints ?? {};
|
|
226
|
+
const endpoints = {
|
|
227
|
+
login: resolveEndpoint(options.endpoints?.login ?? bootEndpoints.login, defaults.login, baseUrl),
|
|
228
|
+
logout: resolveEndpoint(options.endpoints?.logout ?? bootEndpoints.logout, defaults.logout, baseUrl),
|
|
229
|
+
me: resolveEndpoint(options.endpoints?.me ?? bootEndpoints.me, defaults.me, baseUrl)
|
|
230
|
+
};
|
|
231
|
+
const key = bootAuth.key || options.key || "";
|
|
232
|
+
const mode = normalizeMode(bootAuth.mode || options.mode);
|
|
233
|
+
const debug = options.debug ?? (typeof import.meta !== "undefined" && !!import.meta.env?.DEV);
|
|
234
|
+
if (!boot.auth && !options.key && !options.mode) {
|
|
235
|
+
logger?.warn("config.missing", "No __PINOOX__.auth and no explicit mode/key overrides", {
|
|
236
|
+
hasBoot: Object.keys(boot).length > 0
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (!key) {
|
|
240
|
+
logger?.warn("config.missing", "auth.key is empty \u2014 token storage will fail until key is provided", {
|
|
241
|
+
bootAuth
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const loginUrl = options.loginUrl ?? bootAuth.loginUrl ?? (strategy === "remote" ? "/account/login" : null);
|
|
245
|
+
const appPath = typeof boot.url?.BASE === "string" && boot.url.BASE || typeof boot.url?.APP === "string" && boot.url.APP || (apiBase.match(/^(\/[^/]+)/)?.[1] ?? null);
|
|
246
|
+
const siteOrigin = resolveSiteOrigin(boot.url, options.siteOrigin);
|
|
247
|
+
const resolved = {
|
|
248
|
+
mode,
|
|
249
|
+
key,
|
|
250
|
+
provider: bootAuth.provider ?? null,
|
|
251
|
+
source: bootAuth.source ?? null,
|
|
252
|
+
strategy,
|
|
253
|
+
loginUrl,
|
|
254
|
+
baseUrl,
|
|
255
|
+
endpoints,
|
|
256
|
+
apiBase,
|
|
257
|
+
siteOrigin,
|
|
258
|
+
appPath,
|
|
259
|
+
debug: !!debug
|
|
260
|
+
};
|
|
261
|
+
logger?.debug("config.resolved", "Auth config resolved", {
|
|
262
|
+
mode: resolved.mode,
|
|
263
|
+
key: resolved.key,
|
|
264
|
+
provider: resolved.provider,
|
|
265
|
+
source: resolved.source,
|
|
266
|
+
strategy: resolved.strategy,
|
|
267
|
+
baseUrl: resolved.baseUrl,
|
|
268
|
+
endpoints: resolved.endpoints,
|
|
269
|
+
siteOrigin: resolved.siteOrigin,
|
|
270
|
+
appPath: resolved.appPath
|
|
271
|
+
});
|
|
272
|
+
return resolved;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/core/storage.ts
|
|
276
|
+
function readCookie(name) {
|
|
277
|
+
if (typeof document === "undefined") {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=([^;]*)`));
|
|
281
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
282
|
+
}
|
|
283
|
+
function writeCookie(name, value) {
|
|
284
|
+
if (typeof document === "undefined") {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const maxAge = 60 * 60 * 24 * 90;
|
|
288
|
+
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; SameSite=Lax`;
|
|
289
|
+
}
|
|
290
|
+
function deleteCookie(name) {
|
|
291
|
+
if (typeof document === "undefined") {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
document.cookie = `${name}=; path=/; max-age=0; SameSite=Lax`;
|
|
295
|
+
}
|
|
296
|
+
function isDev() {
|
|
297
|
+
try {
|
|
298
|
+
return typeof import.meta !== "undefined" && !!import.meta.env?.DEV;
|
|
299
|
+
} catch {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function createStorage(key, logger) {
|
|
304
|
+
const syncDevCookie = isDev();
|
|
305
|
+
return {
|
|
306
|
+
get: () => {
|
|
307
|
+
if (!key) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
if (typeof localStorage !== "undefined") {
|
|
312
|
+
const fromStorage = localStorage.getItem(key)?.trim() || null;
|
|
313
|
+
if (fromStorage) {
|
|
314
|
+
if (syncDevCookie) {
|
|
315
|
+
writeCookie(key, fromStorage);
|
|
316
|
+
}
|
|
317
|
+
return fromStorage;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
logger?.warn("token.storage", "Failed to read localStorage", { error: String(error) });
|
|
322
|
+
}
|
|
323
|
+
if (!syncDevCookie) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
const fromCookie = readCookie(key);
|
|
327
|
+
if (fromCookie) {
|
|
328
|
+
try {
|
|
329
|
+
localStorage.setItem(key, fromCookie);
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
return fromCookie;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
},
|
|
336
|
+
set: (token) => {
|
|
337
|
+
if (!key) {
|
|
338
|
+
logger?.warn("mismatch.key", "Cannot store token without auth.key");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (typeof localStorage === "undefined") {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
if (token) {
|
|
346
|
+
const value = token.trim();
|
|
347
|
+
localStorage.setItem(key, value);
|
|
348
|
+
logger?.debug("token.stored", "Token stored", { key });
|
|
349
|
+
if (syncDevCookie) {
|
|
350
|
+
writeCookie(key, value);
|
|
351
|
+
}
|
|
352
|
+
} else {
|
|
353
|
+
localStorage.removeItem(key);
|
|
354
|
+
logger?.debug("token.cleared", "Token cleared", { key });
|
|
355
|
+
if (syncDevCookie) {
|
|
356
|
+
deleteCookie(key);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
logger?.warn("token.storage", "Failed to write storage", { error: String(error) });
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
clear: () => {
|
|
364
|
+
if (!key || typeof localStorage === "undefined") {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
localStorage.removeItem(key);
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
if (syncDevCookie) {
|
|
372
|
+
deleteCookie(key);
|
|
373
|
+
}
|
|
374
|
+
logger?.debug("token.cleared", "Token cleared", { key });
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/strategies/local.ts
|
|
380
|
+
function unwrapPayload(data) {
|
|
381
|
+
if (!data || typeof data !== "object") {
|
|
382
|
+
return {};
|
|
383
|
+
}
|
|
384
|
+
const record = data;
|
|
385
|
+
if (record.data && typeof record.data === "object") {
|
|
386
|
+
return record.data;
|
|
387
|
+
}
|
|
388
|
+
return record;
|
|
389
|
+
}
|
|
390
|
+
function extractTokenAndUser(payload) {
|
|
391
|
+
const data = unwrapPayload(payload);
|
|
392
|
+
const tokenCandidate = typeof data.token === "string" && data.token || typeof payload?.token === "string" && payload.token || null;
|
|
393
|
+
const userCandidate = (data.user && typeof data.user === "object" ? data.user : null) || (data.username || data.email || data.user_id ? data : null);
|
|
394
|
+
return {
|
|
395
|
+
token: tokenCandidate,
|
|
396
|
+
user: userCandidate
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
async function localLogin(config, http, storage, credentials, logger) {
|
|
400
|
+
const response = await http.request({
|
|
401
|
+
url: config.endpoints.login,
|
|
402
|
+
method: "POST",
|
|
403
|
+
body: credentials,
|
|
404
|
+
credentials: "include",
|
|
405
|
+
headers: {
|
|
406
|
+
"Content-Type": "application/json",
|
|
407
|
+
"X-Requested-With": "XMLHttpRequest"
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
if (!response.ok) {
|
|
411
|
+
logger?.error("session.unauthorized", "Login request failed", {
|
|
412
|
+
status: response.status,
|
|
413
|
+
url: config.endpoints.login
|
|
414
|
+
});
|
|
415
|
+
throw Object.assign(new Error("Login failed"), { status: response.status, data: response.data });
|
|
416
|
+
}
|
|
417
|
+
const { token, user } = extractTokenAndUser(response.data);
|
|
418
|
+
if (config.mode === "jwt" && !token) {
|
|
419
|
+
logger?.warn("mismatch.mode", "JWT mode expected token in login response but none found", {
|
|
420
|
+
mode: config.mode,
|
|
421
|
+
url: config.endpoints.login
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (token) {
|
|
425
|
+
storage.set(token);
|
|
426
|
+
}
|
|
427
|
+
logger?.info("session.ok", "Login succeeded", { hasToken: !!token, hasUser: !!user });
|
|
428
|
+
return { token, user, raw: response.data };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/strategies/remote.ts
|
|
432
|
+
function buildRemoteLoginUrl(config, returnPath, logger) {
|
|
433
|
+
const loginUrl = config.loginUrl ?? "/account/login";
|
|
434
|
+
const blocked = config.appPath ? [config.appPath, "/account"] : ["/account"];
|
|
435
|
+
const safeRedirect = resolveReturnPath(
|
|
436
|
+
returnPath ?? (typeof window !== "undefined" ? `${window.location.pathname}${window.location.search}` : "/"),
|
|
437
|
+
"/",
|
|
438
|
+
blocked
|
|
439
|
+
);
|
|
440
|
+
const url = `${loginUrl}?redirect=${encodeURIComponent(safeRedirect)}`;
|
|
441
|
+
logger?.info("redirect.login", "Redirecting to remote login", { url, returnPath: safeRedirect });
|
|
442
|
+
return url;
|
|
443
|
+
}
|
|
444
|
+
function redirectToRemoteLogin(config, returnPath, logger) {
|
|
445
|
+
if (typeof window === "undefined") {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
window.location.href = buildRemoteLoginUrl(config, returnPath, logger);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/http/types.ts
|
|
452
|
+
function createFetchProvider() {
|
|
453
|
+
return {
|
|
454
|
+
async request(input) {
|
|
455
|
+
const headers = {
|
|
456
|
+
Accept: "application/json",
|
|
457
|
+
...input.headers
|
|
458
|
+
};
|
|
459
|
+
let body;
|
|
460
|
+
if (input.body !== void 0 && input.body !== null) {
|
|
461
|
+
if (typeof input.body === "string" || input.body instanceof FormData || input.body instanceof Blob) {
|
|
462
|
+
body = input.body;
|
|
463
|
+
} else {
|
|
464
|
+
headers["Content-Type"] = headers["Content-Type"] ?? "application/json";
|
|
465
|
+
body = JSON.stringify(input.body);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const response = await fetch(input.url, {
|
|
469
|
+
method: input.method ?? "GET",
|
|
470
|
+
headers,
|
|
471
|
+
body,
|
|
472
|
+
credentials: input.credentials ?? "include"
|
|
473
|
+
});
|
|
474
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
475
|
+
let data;
|
|
476
|
+
if (contentType.includes("application/json")) {
|
|
477
|
+
data = await response.json();
|
|
478
|
+
} else {
|
|
479
|
+
data = await response.text();
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
status: response.status,
|
|
483
|
+
data,
|
|
484
|
+
headers: response.headers,
|
|
485
|
+
ok: response.ok
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/core/createAuth.ts
|
|
492
|
+
var defaultInstance = null;
|
|
493
|
+
function getAuth() {
|
|
494
|
+
if (!defaultInstance) {
|
|
495
|
+
throw new Error("@pinooxhq/auth: call createAuth() before getAuth()");
|
|
496
|
+
}
|
|
497
|
+
return defaultInstance;
|
|
498
|
+
}
|
|
499
|
+
function createAuth(options = {}) {
|
|
500
|
+
const logger = options.logger ?? createLogger(options.debug);
|
|
501
|
+
const config = resolveConfig(options, logger);
|
|
502
|
+
const storage = createStorage(config.key, logger);
|
|
503
|
+
let http = options.http ?? createFetchProvider();
|
|
504
|
+
let token = storage.get();
|
|
505
|
+
let user = null;
|
|
506
|
+
let authenticated = false;
|
|
507
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
508
|
+
const emit = (event, payload) => {
|
|
509
|
+
const set = listeners.get(event);
|
|
510
|
+
if (!set) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
for (const handler of set) {
|
|
514
|
+
try {
|
|
515
|
+
handler(payload);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
logger.error("error", "Event handler failed", { event, error: String(error) });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
const syncToken = (value) => {
|
|
522
|
+
token = value;
|
|
523
|
+
storage.set(value);
|
|
524
|
+
};
|
|
525
|
+
const getAuthHeader = () => {
|
|
526
|
+
if (config.mode !== "jwt" || !token) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
return token.toLowerCase().startsWith("bearer ") ? token : `Bearer ${token}`;
|
|
530
|
+
};
|
|
531
|
+
const getRequestAuth = () => ({
|
|
532
|
+
header: getAuthHeader(),
|
|
533
|
+
credentials: "include",
|
|
534
|
+
mode: config.mode,
|
|
535
|
+
key: config.key
|
|
536
|
+
});
|
|
537
|
+
const authorize = (headers = {}) => {
|
|
538
|
+
const auth = getRequestAuth();
|
|
539
|
+
const next = { ...headers };
|
|
540
|
+
if (auth.header) {
|
|
541
|
+
next.Authorization = auth.header;
|
|
542
|
+
}
|
|
543
|
+
logger.debug("request.auth", "Authorized request headers", {
|
|
544
|
+
mode: auth.mode,
|
|
545
|
+
hasHeader: !!auth.header
|
|
546
|
+
});
|
|
547
|
+
return { ...auth, headers: next };
|
|
548
|
+
};
|
|
549
|
+
const withAuthHeaders = (headers = {}) => {
|
|
550
|
+
return authorize(headers).headers;
|
|
551
|
+
};
|
|
552
|
+
const loginFromResponse = (payload) => {
|
|
553
|
+
const extracted = extractTokenAndUser(payload);
|
|
554
|
+
if (config.mode === "jwt" && !extracted.token) {
|
|
555
|
+
logger.warn("mismatch.mode", "JWT mode expected token in response but none found", {
|
|
556
|
+
mode: config.mode
|
|
557
|
+
});
|
|
558
|
+
emit("mismatch", { code: "mismatch.mode", payload });
|
|
559
|
+
}
|
|
560
|
+
if (extracted.token) {
|
|
561
|
+
syncToken(extracted.token);
|
|
562
|
+
}
|
|
563
|
+
if (extracted.user) {
|
|
564
|
+
user = extracted.user;
|
|
565
|
+
}
|
|
566
|
+
authenticated = !!(extracted.token || config.mode !== "jwt");
|
|
567
|
+
emit("login", { token: extracted.token, user: extracted.user });
|
|
568
|
+
return { ...extracted, raw: payload };
|
|
569
|
+
};
|
|
570
|
+
const me = async () => {
|
|
571
|
+
const current = storage.get();
|
|
572
|
+
token = current;
|
|
573
|
+
if (config.mode === "jwt" && !current) {
|
|
574
|
+
authenticated = false;
|
|
575
|
+
user = null;
|
|
576
|
+
logger.debug("session.unauthorized", "No token for me()", { mode: config.mode });
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
const response = await http.request({
|
|
581
|
+
url: config.endpoints.me,
|
|
582
|
+
method: "GET",
|
|
583
|
+
credentials: "include",
|
|
584
|
+
headers: withAuthHeaders({
|
|
585
|
+
Accept: "application/json",
|
|
586
|
+
"X-Requested-With": "XMLHttpRequest"
|
|
587
|
+
})
|
|
588
|
+
});
|
|
589
|
+
if (response.status === 401) {
|
|
590
|
+
syncToken(null);
|
|
591
|
+
user = null;
|
|
592
|
+
authenticated = false;
|
|
593
|
+
logger.warn("session.unauthorized", "Session validation returned 401", {
|
|
594
|
+
url: config.endpoints.me
|
|
595
|
+
});
|
|
596
|
+
emit("unauthorized", { status: 401 });
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
if (!response.ok) {
|
|
600
|
+
logger.error("error", "me() request failed", { status: response.status });
|
|
601
|
+
emit("error", { status: response.status, data: response.data });
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
const extracted = extractTokenAndUser(response.data);
|
|
605
|
+
user = extracted.user ?? response.data;
|
|
606
|
+
authenticated = true;
|
|
607
|
+
logger.info("session.ok", "Session validated", { hasUser: !!user });
|
|
608
|
+
return user;
|
|
609
|
+
} catch (error) {
|
|
610
|
+
logger.error("error", "me() threw", { error: String(error) });
|
|
611
|
+
emit("error", error);
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
const login = async (credentials) => {
|
|
616
|
+
if (config.strategy === "remote") {
|
|
617
|
+
redirectToRemoteLogin(config, void 0, logger);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (!credentials?.password) {
|
|
621
|
+
throw new Error("@pinooxhq/auth: login() requires credentials for local strategy");
|
|
622
|
+
}
|
|
623
|
+
const result = await localLogin(config, http, storage, credentials, logger);
|
|
624
|
+
token = result.token ?? storage.get();
|
|
625
|
+
user = result.user;
|
|
626
|
+
authenticated = !!(result.token || config.mode !== "jwt");
|
|
627
|
+
emit("login", result);
|
|
628
|
+
return result;
|
|
629
|
+
};
|
|
630
|
+
const logout = async () => {
|
|
631
|
+
try {
|
|
632
|
+
await http.request({
|
|
633
|
+
url: config.endpoints.logout,
|
|
634
|
+
method: "GET",
|
|
635
|
+
credentials: "include",
|
|
636
|
+
headers: withAuthHeaders({
|
|
637
|
+
Accept: "application/json",
|
|
638
|
+
"X-Requested-With": "XMLHttpRequest"
|
|
639
|
+
})
|
|
640
|
+
});
|
|
641
|
+
} catch (error) {
|
|
642
|
+
logger.warn("error", "Remote logout failed; clearing local session anyway", {
|
|
643
|
+
error: String(error)
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
syncToken(null);
|
|
647
|
+
user = null;
|
|
648
|
+
authenticated = false;
|
|
649
|
+
emit("logout");
|
|
650
|
+
logger.info("token.cleared", "Logged out");
|
|
651
|
+
};
|
|
652
|
+
const instance = {
|
|
653
|
+
config,
|
|
654
|
+
logger,
|
|
655
|
+
get user() {
|
|
656
|
+
return user;
|
|
657
|
+
},
|
|
658
|
+
set user(value) {
|
|
659
|
+
user = value;
|
|
660
|
+
},
|
|
661
|
+
get token() {
|
|
662
|
+
return token ?? storage.get();
|
|
663
|
+
},
|
|
664
|
+
set token(value) {
|
|
665
|
+
syncToken(value);
|
|
666
|
+
},
|
|
667
|
+
get isAuthenticated() {
|
|
668
|
+
return authenticated && (!!token || config.mode !== "jwt");
|
|
669
|
+
},
|
|
670
|
+
set isAuthenticated(value) {
|
|
671
|
+
authenticated = value;
|
|
672
|
+
},
|
|
673
|
+
login,
|
|
674
|
+
logout,
|
|
675
|
+
me,
|
|
676
|
+
getToken: () => token ?? storage.get(),
|
|
677
|
+
setToken: (value) => {
|
|
678
|
+
syncToken(value);
|
|
679
|
+
},
|
|
680
|
+
clearToken: () => {
|
|
681
|
+
syncToken(null);
|
|
682
|
+
authenticated = false;
|
|
683
|
+
},
|
|
684
|
+
getAuthHeader,
|
|
685
|
+
authorize,
|
|
686
|
+
getRequestAuth,
|
|
687
|
+
setHttp: (client) => {
|
|
688
|
+
http = client;
|
|
689
|
+
},
|
|
690
|
+
loginFromResponse,
|
|
691
|
+
redirectToLogin: (returnPath) => {
|
|
692
|
+
if (config.strategy === "remote") {
|
|
693
|
+
redirectToRemoteLogin(config, returnPath, logger);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const url = config.loginUrl ?? config.endpoints.login;
|
|
697
|
+
logger.info("redirect.login", "Redirecting to login", { url });
|
|
698
|
+
if (typeof window !== "undefined") {
|
|
699
|
+
window.location.href = url;
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
getReturnPath: (queryOrPath, fallback = "/") => {
|
|
703
|
+
const blocked = config.appPath ? [config.appPath] : ["/account"];
|
|
704
|
+
return resolveReturnPath(queryOrPath, fallback, blocked);
|
|
705
|
+
},
|
|
706
|
+
getReturnUrl: (queryOrPath, fallback = "/") => {
|
|
707
|
+
const blocked = config.appPath ? [config.appPath] : ["/account"];
|
|
708
|
+
const path = resolveReturnPath(queryOrPath, fallback, blocked);
|
|
709
|
+
return toAbsoluteReturnUrl(path, config.siteOrigin);
|
|
710
|
+
},
|
|
711
|
+
redirectBack: (queryOrPath, fallback = "/") => {
|
|
712
|
+
const blocked = config.appPath ? [config.appPath] : ["/account"];
|
|
713
|
+
logger.info("redirect.back", "Redirecting after auth", { queryOrPath, fallback });
|
|
714
|
+
redirectToReturn(queryOrPath, {
|
|
715
|
+
fallback,
|
|
716
|
+
siteOrigin: config.siteOrigin,
|
|
717
|
+
blockedPrefixes: blocked
|
|
718
|
+
});
|
|
719
|
+
},
|
|
720
|
+
getRedirectQuery: (queryOrPath, fallback = "/") => {
|
|
721
|
+
const blocked = config.appPath ? [config.appPath] : ["/account"];
|
|
722
|
+
return { redirect: resolveReturnPath(queryOrPath, fallback, blocked) };
|
|
723
|
+
},
|
|
724
|
+
notifyUnauthorized: (payload) => {
|
|
725
|
+
syncToken(null);
|
|
726
|
+
user = null;
|
|
727
|
+
authenticated = false;
|
|
728
|
+
logger.warn("session.unauthorized", "Unauthorized notified", {
|
|
729
|
+
payload: payload ? String(payload) : void 0
|
|
730
|
+
});
|
|
731
|
+
emit("unauthorized", payload ?? { status: 401 });
|
|
732
|
+
},
|
|
733
|
+
on: (event, handler) => {
|
|
734
|
+
if (!listeners.has(event)) {
|
|
735
|
+
listeners.set(event, /* @__PURE__ */ new Set());
|
|
736
|
+
}
|
|
737
|
+
listeners.get(event).add(handler);
|
|
738
|
+
return () => {
|
|
739
|
+
listeners.get(event)?.delete(handler);
|
|
740
|
+
};
|
|
741
|
+
},
|
|
742
|
+
off: (event, handler) => {
|
|
743
|
+
listeners.get(event)?.delete(handler);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
defaultInstance = instance;
|
|
747
|
+
emit("config", config);
|
|
748
|
+
return instance;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/svelte/useAuth.ts
|
|
752
|
+
var bound = null;
|
|
753
|
+
function resolveAuthInstance(options) {
|
|
754
|
+
try {
|
|
755
|
+
return getAuth();
|
|
756
|
+
} catch {
|
|
757
|
+
return createAuth(options ?? {});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function readQueryFromWindow() {
|
|
761
|
+
if (typeof window === "undefined") {
|
|
762
|
+
return {};
|
|
763
|
+
}
|
|
764
|
+
return Object.fromEntries(new URLSearchParams(window.location.search).entries());
|
|
765
|
+
}
|
|
766
|
+
function normalizeQuery(input) {
|
|
767
|
+
if (typeof input === "string") {
|
|
768
|
+
const raw = input.startsWith("?") ? input.slice(1) : input;
|
|
769
|
+
return Object.fromEntries(new URLSearchParams(raw).entries());
|
|
770
|
+
}
|
|
771
|
+
if (input instanceof URLSearchParams) {
|
|
772
|
+
return Object.fromEntries(input.entries());
|
|
773
|
+
}
|
|
774
|
+
return input;
|
|
775
|
+
}
|
|
776
|
+
function createAuthStore(options = {}) {
|
|
777
|
+
if (bound) {
|
|
778
|
+
return bound;
|
|
779
|
+
}
|
|
780
|
+
const auth = resolveAuthInstance(options);
|
|
781
|
+
const user = writable(auth.user);
|
|
782
|
+
const token = writable(auth.getToken());
|
|
783
|
+
const syncFromAuth = () => {
|
|
784
|
+
user.set(auth.user);
|
|
785
|
+
token.set(auth.getToken());
|
|
786
|
+
};
|
|
787
|
+
auth.on("login", syncFromAuth);
|
|
788
|
+
auth.on("logout", syncFromAuth);
|
|
789
|
+
auth.on("unauthorized", syncFromAuth);
|
|
790
|
+
const isAuthenticated = derived([user, token], ([$user, $token]) => {
|
|
791
|
+
return auth.isAuthenticated || !!$token && !!$user;
|
|
792
|
+
});
|
|
793
|
+
const me = async () => {
|
|
794
|
+
const profile = await auth.me();
|
|
795
|
+
syncFromAuth();
|
|
796
|
+
return profile;
|
|
797
|
+
};
|
|
798
|
+
const canAccess = async (refresh = false) => {
|
|
799
|
+
token.set(auth.getToken());
|
|
800
|
+
if (!refresh && auth.isAuthenticated) {
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
if (!auth.getToken() && auth.config.mode === "jwt") {
|
|
804
|
+
auth.isAuthenticated = false;
|
|
805
|
+
user.set(null);
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
const profile = await me();
|
|
809
|
+
return !!profile || auth.isAuthenticated;
|
|
810
|
+
};
|
|
811
|
+
const login = async (credentials) => {
|
|
812
|
+
const result = await auth.login(credentials);
|
|
813
|
+
syncFromAuth();
|
|
814
|
+
return result;
|
|
815
|
+
};
|
|
816
|
+
const logout = async () => {
|
|
817
|
+
await auth.logout();
|
|
818
|
+
syncFromAuth();
|
|
819
|
+
};
|
|
820
|
+
const setSession = (loginKey, userData = null) => {
|
|
821
|
+
auth.setToken(loginKey);
|
|
822
|
+
auth.isAuthenticated = true;
|
|
823
|
+
token.set(loginKey);
|
|
824
|
+
if (userData) {
|
|
825
|
+
auth.user = userData;
|
|
826
|
+
user.set(userData);
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
bound = {
|
|
830
|
+
auth,
|
|
831
|
+
user,
|
|
832
|
+
token,
|
|
833
|
+
isAuthenticated,
|
|
834
|
+
login,
|
|
835
|
+
logout,
|
|
836
|
+
me,
|
|
837
|
+
canAccess,
|
|
838
|
+
setSession
|
|
839
|
+
};
|
|
840
|
+
return bound;
|
|
841
|
+
}
|
|
842
|
+
function useAuth(options) {
|
|
843
|
+
return createAuthStore(options ?? {});
|
|
844
|
+
}
|
|
845
|
+
function createAuthRedirect(getQuery = readQueryFromWindow, fallback = "/") {
|
|
846
|
+
const auth = resolveAuthInstance();
|
|
847
|
+
const readPath = () => auth.getReturnPath(normalizeQuery(getQuery()), fallback);
|
|
848
|
+
const readUrl = () => auth.getReturnUrl(normalizeQuery(getQuery()), fallback);
|
|
849
|
+
const readRedirectQuery = () => auth.getRedirectQuery(normalizeQuery(getQuery()), fallback);
|
|
850
|
+
const returnPath = {
|
|
851
|
+
subscribe(run) {
|
|
852
|
+
run(readPath());
|
|
853
|
+
return () => void 0;
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
const returnUrl = {
|
|
857
|
+
subscribe(run) {
|
|
858
|
+
run(readUrl());
|
|
859
|
+
return () => void 0;
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
const redirectQuery = {
|
|
863
|
+
subscribe(run) {
|
|
864
|
+
run(readRedirectQuery());
|
|
865
|
+
return () => void 0;
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
return {
|
|
869
|
+
returnPath,
|
|
870
|
+
returnUrl,
|
|
871
|
+
redirectQuery,
|
|
872
|
+
resolveRedirect: readUrl,
|
|
873
|
+
redirectBack: () => auth.redirectBack(normalizeQuery(getQuery()), fallback)
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function createAuthRedirectFromStore(queryStore, fallback = "/") {
|
|
877
|
+
const auth = resolveAuthInstance();
|
|
878
|
+
let latest = {};
|
|
879
|
+
const unsub = queryStore.subscribe((value) => {
|
|
880
|
+
latest = value;
|
|
881
|
+
});
|
|
882
|
+
unsub();
|
|
883
|
+
const returnPath = derived(queryStore, ($q) => {
|
|
884
|
+
latest = $q;
|
|
885
|
+
return auth.getReturnPath(normalizeQuery($q), fallback);
|
|
886
|
+
});
|
|
887
|
+
const returnUrl = derived(queryStore, ($q) => {
|
|
888
|
+
latest = $q;
|
|
889
|
+
return auth.getReturnUrl(normalizeQuery($q), fallback);
|
|
890
|
+
});
|
|
891
|
+
const redirectQuery = derived(queryStore, ($q) => {
|
|
892
|
+
latest = $q;
|
|
893
|
+
return auth.getRedirectQuery(normalizeQuery($q), fallback);
|
|
894
|
+
});
|
|
895
|
+
return {
|
|
896
|
+
returnPath,
|
|
897
|
+
returnUrl,
|
|
898
|
+
redirectQuery,
|
|
899
|
+
resolveRedirect: () => auth.getReturnUrl(normalizeQuery(latest), fallback),
|
|
900
|
+
redirectBack: () => auth.redirectBack(normalizeQuery(latest), fallback)
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function createSvelteAuth(options = {}) {
|
|
904
|
+
const store = createAuthStore(options);
|
|
905
|
+
return {
|
|
906
|
+
...store,
|
|
907
|
+
useAuth: () => store,
|
|
908
|
+
createAuthRedirect,
|
|
909
|
+
createAuthRedirectFromStore
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
export { createAuthRedirect, createAuthRedirectFromStore, createAuthStore, createSvelteAuth, useAuth };
|
|
914
|
+
//# sourceMappingURL=index.js.map
|
|
915
|
+
//# sourceMappingURL=index.js.map
|