@authyon/auth 0.2.0-beta.0 → 0.2.0-beta.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 +21 -0
- package/README.md +177 -14
- package/dist/ability-g6nBpOQM.d.cts +617 -0
- package/dist/ability-g6nBpOQM.d.ts +617 -0
- package/dist/chunk-EHZEUM47.js +323 -0
- package/dist/index.cjs +748 -60
- package/dist/index.d.cts +57 -466
- package/dist/index.d.ts +57 -466
- package/dist/index.js +431 -58
- package/dist/react/index.cjs +428 -0
- package/dist/react/index.d.cts +47 -0
- package/dist/react/index.d.ts +47 -0
- package/dist/react/index.js +120 -0
- package/package.json +71 -37
package/dist/index.js
CHANGED
|
@@ -1,26 +1,128 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
|
+
AuthyonAbility,
|
|
3
|
+
AuthyonAbilityBuilder,
|
|
4
|
+
AuthyonSessionController,
|
|
5
|
+
createAuthyonAbility,
|
|
6
|
+
createAuthyonRules,
|
|
7
|
+
hasPermission
|
|
8
|
+
} from "./chunk-EHZEUM47.js";
|
|
9
|
+
|
|
10
|
+
// ../../internal/core/errors/authyonError.ts
|
|
11
|
+
var ErrorCodes = {
|
|
12
|
+
Unknown: "unknown",
|
|
13
|
+
NetworkError: "request.network_error",
|
|
14
|
+
Timeout: "request.timeout",
|
|
15
|
+
SessionMalformed: "session.malformed",
|
|
16
|
+
NotAuthenticated: "auth.not_authenticated",
|
|
17
|
+
InvalidToken: "auth.invalid_token",
|
|
18
|
+
MissingToken: "auth.missing_token",
|
|
19
|
+
EmailTaken: "user.email_taken",
|
|
20
|
+
PasswordWeak: "user.password_weak",
|
|
21
|
+
PasswordPwned: "user.password_pwned",
|
|
22
|
+
RateLimited: "rate_limited"
|
|
23
|
+
};
|
|
2
24
|
var AuthyonError = class extends Error {
|
|
3
|
-
constructor(status, body) {
|
|
25
|
+
constructor(status, body, options = {}) {
|
|
4
26
|
super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
|
|
5
27
|
this.name = "AuthyonError";
|
|
6
28
|
this.status = status;
|
|
7
29
|
this.code = body.code ?? "unknown";
|
|
8
30
|
this.title = body.title ?? "Error";
|
|
9
31
|
this.detail = body.detail;
|
|
32
|
+
this.requestId = options.requestId;
|
|
33
|
+
this.retryAfter = options.retryAfter;
|
|
34
|
+
this.cause = options.cause;
|
|
10
35
|
}
|
|
11
36
|
is(code) {
|
|
12
37
|
return this.code === code;
|
|
13
38
|
}
|
|
39
|
+
isAny(...codes) {
|
|
40
|
+
return codes.includes(this.code);
|
|
41
|
+
}
|
|
42
|
+
hasPrefix(prefix) {
|
|
43
|
+
return this.code.startsWith(prefix);
|
|
44
|
+
}
|
|
45
|
+
isStatus(...statuses) {
|
|
46
|
+
return statuses.includes(this.status);
|
|
47
|
+
}
|
|
48
|
+
/** Stable interpretation for UI decisions, retries, telemetry and support flows. */
|
|
49
|
+
interpret() {
|
|
50
|
+
const category = classifyError(this.status, this.code);
|
|
51
|
+
return {
|
|
52
|
+
category,
|
|
53
|
+
action: actionFor(category),
|
|
54
|
+
retryable: isRetryable(category),
|
|
55
|
+
...this.retryAfter !== void 0 ? { retryAfter: this.retryAfter } : {},
|
|
56
|
+
...this.requestId !== void 0 ? { requestId: this.requestId } : {}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
get category() {
|
|
60
|
+
return this.interpret().category;
|
|
61
|
+
}
|
|
62
|
+
get retryable() {
|
|
63
|
+
return this.interpret().retryable;
|
|
64
|
+
}
|
|
65
|
+
toJSON() {
|
|
66
|
+
return {
|
|
67
|
+
name: this.name,
|
|
68
|
+
message: this.message,
|
|
69
|
+
status: this.status,
|
|
70
|
+
code: this.code,
|
|
71
|
+
title: this.title,
|
|
72
|
+
detail: this.detail,
|
|
73
|
+
requestId: this.requestId,
|
|
74
|
+
retryAfter: this.retryAfter,
|
|
75
|
+
...this.interpret()
|
|
76
|
+
};
|
|
77
|
+
}
|
|
14
78
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
79
|
+
function classifyError(status, code) {
|
|
80
|
+
if (code === ErrorCodes.NetworkError) return "network";
|
|
81
|
+
if (code === ErrorCodes.Timeout || status === 408) return "timeout";
|
|
82
|
+
if (code === ErrorCodes.RateLimited || status === 429) return "rate_limit";
|
|
83
|
+
if (code === ErrorCodes.EmailTaken || status === 409) return "conflict";
|
|
84
|
+
if (code === ErrorCodes.PasswordWeak || code === ErrorCodes.PasswordPwned) return "validation";
|
|
85
|
+
if (code === ErrorCodes.SessionMalformed) return "server";
|
|
86
|
+
if (code.startsWith("auth.") || status === 401) return "authentication";
|
|
87
|
+
if (status === 403) return "authorization";
|
|
88
|
+
if (status === 404) return "not_found";
|
|
89
|
+
if (status === 400 || status === 422) return "validation";
|
|
90
|
+
if (status >= 500) return "server";
|
|
91
|
+
return "unknown";
|
|
92
|
+
}
|
|
93
|
+
function actionFor(category) {
|
|
94
|
+
switch (category) {
|
|
95
|
+
case "network":
|
|
96
|
+
case "timeout":
|
|
97
|
+
case "rate_limit":
|
|
98
|
+
case "server":
|
|
99
|
+
return "retry";
|
|
100
|
+
case "authentication":
|
|
101
|
+
return "reauthenticate";
|
|
102
|
+
case "authorization":
|
|
103
|
+
return "request_access";
|
|
104
|
+
case "validation":
|
|
105
|
+
return "fix_input";
|
|
106
|
+
case "not_found":
|
|
107
|
+
return "not_found";
|
|
108
|
+
case "conflict":
|
|
109
|
+
return "resolve_conflict";
|
|
110
|
+
case "unknown":
|
|
111
|
+
return "contact_support";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function isRetryable(category) {
|
|
115
|
+
return category === "network" || category === "timeout" || category === "rate_limit" || category === "server";
|
|
116
|
+
}
|
|
20
117
|
|
|
21
|
-
// src/storage.ts
|
|
118
|
+
// src/session/storage.ts
|
|
22
119
|
var STORAGE_KEY = "authyon.session";
|
|
23
|
-
function
|
|
120
|
+
function isSession(value) {
|
|
121
|
+
if (!value || typeof value !== "object") return false;
|
|
122
|
+
const candidate = value;
|
|
123
|
+
return typeof candidate.accessToken === "string" && candidate.accessToken.length > 0 && typeof candidate.refreshToken === "string" && candidate.refreshToken.length > 0 && typeof candidate.expiresIn === "number" && Number.isFinite(candidate.expiresIn) && candidate.expiresIn > 0 && typeof candidate.expiresAt === "number" && Number.isFinite(candidate.expiresAt);
|
|
124
|
+
}
|
|
125
|
+
function createMemoryStorage() {
|
|
24
126
|
let session = null;
|
|
25
127
|
return {
|
|
26
128
|
get: () => session,
|
|
@@ -32,12 +134,16 @@ function memoryStorage() {
|
|
|
32
134
|
}
|
|
33
135
|
};
|
|
34
136
|
}
|
|
35
|
-
function
|
|
137
|
+
function createLocalStorage(key = STORAGE_KEY) {
|
|
36
138
|
return {
|
|
37
139
|
get() {
|
|
38
140
|
try {
|
|
39
141
|
const raw = window.localStorage.getItem(key);
|
|
40
|
-
|
|
142
|
+
if (!raw) return null;
|
|
143
|
+
const parsed = JSON.parse(raw);
|
|
144
|
+
if (isSession(parsed)) return parsed;
|
|
145
|
+
window.localStorage.removeItem(key);
|
|
146
|
+
return null;
|
|
41
147
|
} catch {
|
|
42
148
|
return null;
|
|
43
149
|
}
|
|
@@ -56,22 +162,217 @@ function localStorageAdapter(key = STORAGE_KEY) {
|
|
|
56
162
|
}
|
|
57
163
|
};
|
|
58
164
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
return localStorageAdapter();
|
|
62
|
-
}
|
|
63
|
-
return memoryStorage();
|
|
165
|
+
function createDefaultStorage() {
|
|
166
|
+
return createMemoryStorage();
|
|
64
167
|
}
|
|
65
168
|
|
|
66
|
-
//
|
|
169
|
+
// ../../internal/core/config/defaults.ts
|
|
67
170
|
var DEFAULT_BASE_URL = "https://api.authyon.com";
|
|
68
|
-
var
|
|
171
|
+
var DEFAULT_EXPIRY_SKEW_MS = 3e4;
|
|
172
|
+
|
|
173
|
+
// ../../internal/core/http/query.ts
|
|
174
|
+
function appendQuery(path, params) {
|
|
175
|
+
if (!params) return path;
|
|
176
|
+
const query = new URLSearchParams();
|
|
177
|
+
for (const [key, value2] of Object.entries(params)) {
|
|
178
|
+
if (value2 !== void 0) query.set(key, String(value2));
|
|
179
|
+
}
|
|
180
|
+
const value = query.toString();
|
|
181
|
+
return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ../../internal/core/http/httpAdapter.ts
|
|
185
|
+
var FetchHttpAdapter = class {
|
|
186
|
+
constructor(fetchImpl = fetch.bind(globalThis)) {
|
|
187
|
+
this.fetchImpl = fetchImpl;
|
|
188
|
+
}
|
|
189
|
+
request(request) {
|
|
190
|
+
return this.fetchImpl(request.url, {
|
|
191
|
+
method: request.method,
|
|
192
|
+
headers: request.headers,
|
|
193
|
+
body: request.body,
|
|
194
|
+
signal: request.signal
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
var LoggingHttpAdapter = class {
|
|
199
|
+
constructor(adapter, options) {
|
|
200
|
+
this.adapter = adapter;
|
|
201
|
+
this.options = options;
|
|
202
|
+
}
|
|
203
|
+
async request(request) {
|
|
204
|
+
const startedAt = Date.now();
|
|
205
|
+
const eventBase = {
|
|
206
|
+
method: request.method,
|
|
207
|
+
url: sanitizeUrl(request.url)
|
|
208
|
+
};
|
|
209
|
+
this.log({ type: "request", ...eventBase, timestamp: startedAt });
|
|
210
|
+
try {
|
|
211
|
+
const response = await this.adapter.request(request);
|
|
212
|
+
this.log({
|
|
213
|
+
type: "response",
|
|
214
|
+
...eventBase,
|
|
215
|
+
status: response.status,
|
|
216
|
+
durationMs: Date.now() - startedAt,
|
|
217
|
+
requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
|
|
218
|
+
timestamp: Date.now()
|
|
219
|
+
});
|
|
220
|
+
return response;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
this.log({
|
|
223
|
+
type: "error",
|
|
224
|
+
...eventBase,
|
|
225
|
+
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
226
|
+
durationMs: Date.now() - startedAt,
|
|
227
|
+
timestamp: Date.now()
|
|
228
|
+
});
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
log(event) {
|
|
233
|
+
if (!this.options.enabled) return;
|
|
234
|
+
const logger = this.options.logger ?? defaultHttpLogger;
|
|
235
|
+
try {
|
|
236
|
+
logger(event);
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
function defaultHttpLogger(event) {
|
|
242
|
+
console.debug("[Authyon HTTP]", event);
|
|
243
|
+
}
|
|
244
|
+
function sanitizeUrl(value) {
|
|
245
|
+
const url = new URL(value);
|
|
246
|
+
const queryKeys = /* @__PURE__ */ new Set();
|
|
247
|
+
url.searchParams.forEach((_value, key) => queryKeys.add(key));
|
|
248
|
+
url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
|
|
249
|
+
return url.toString();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ../../internal/core/http/transport.ts
|
|
253
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
254
|
+
var SharedTransportError = class extends Error {
|
|
255
|
+
constructor(code, cause) {
|
|
256
|
+
super(code === "request.timeout" ? "Request timed out" : "Network request failed");
|
|
257
|
+
this.name = "SharedTransportError";
|
|
258
|
+
this.code = code;
|
|
259
|
+
this.cause = cause;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
function createSharedTransport(options) {
|
|
263
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl, options.allowInsecureHttp ?? false);
|
|
264
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
265
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
266
|
+
throw new Error("Authyon: `timeoutMs` must be a non-negative finite number");
|
|
267
|
+
}
|
|
268
|
+
if (options.httpAdapter && options.fetch) {
|
|
269
|
+
throw new Error("Authyon: use either `httpAdapter` or `fetch`, not both");
|
|
270
|
+
}
|
|
271
|
+
const baseAdapter = options.httpAdapter ?? new FetchHttpAdapter(options.fetch);
|
|
272
|
+
const httpAdapter = options.httpLogger ? new LoggingHttpAdapter(baseAdapter, options.httpLogger) : baseAdapter;
|
|
273
|
+
return {
|
|
274
|
+
baseUrl,
|
|
275
|
+
async request(path, init) {
|
|
276
|
+
const controller = new AbortController();
|
|
277
|
+
const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
278
|
+
try {
|
|
279
|
+
return await httpAdapter.request({
|
|
280
|
+
url: `${baseUrl}${path}`,
|
|
281
|
+
method: init.method ?? "GET",
|
|
282
|
+
headers: normalizeHeaders(init.headers),
|
|
283
|
+
body: init.body,
|
|
284
|
+
signal: controller.signal
|
|
285
|
+
});
|
|
286
|
+
} catch (cause) {
|
|
287
|
+
throw new SharedTransportError(
|
|
288
|
+
controller.signal.aborted ? ErrorCodes.Timeout : ErrorCodes.NetworkError,
|
|
289
|
+
cause
|
|
290
|
+
);
|
|
291
|
+
} finally {
|
|
292
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function normalizeHeaders(headers) {
|
|
298
|
+
const normalized = {};
|
|
299
|
+
new Headers(headers).forEach((value, key) => {
|
|
300
|
+
normalized[key] = value;
|
|
301
|
+
});
|
|
302
|
+
return normalized;
|
|
303
|
+
}
|
|
304
|
+
function responseMetadata(response) {
|
|
305
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
306
|
+
const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
|
|
307
|
+
return {
|
|
308
|
+
requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
|
|
309
|
+
retryAfter: Number.isFinite(retryAfter) ? retryAfter : void 0
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function normalizeBaseUrl(value, allowInsecureHttp) {
|
|
313
|
+
let url;
|
|
314
|
+
try {
|
|
315
|
+
url = new URL(value);
|
|
316
|
+
} catch {
|
|
317
|
+
throw new Error("Authyon: `baseUrl` must be an absolute URL");
|
|
318
|
+
}
|
|
319
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
320
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && (loopback || allowInsecureHttp))) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
"Authyon: `baseUrl` must use HTTPS (HTTP is allowed only for loopback or with `allowInsecureHttp`)"
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
"Authyon: `baseUrl` cannot contain credentials, query parameters, or fragments"
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
return url.toString().replace(/\/+$/, "");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ../../internal/core/http/jsonHttpClient.ts
|
|
334
|
+
var JsonHttpClient = class {
|
|
335
|
+
constructor(transport) {
|
|
336
|
+
this.transport = transport;
|
|
337
|
+
}
|
|
338
|
+
async send(path, options = {}) {
|
|
339
|
+
const headers = { ...options.headers };
|
|
340
|
+
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
341
|
+
try {
|
|
342
|
+
return await this.transport.request(appendQuery(path, options.query), {
|
|
343
|
+
method: options.method ?? "GET",
|
|
344
|
+
headers,
|
|
345
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
346
|
+
});
|
|
347
|
+
} catch (cause) {
|
|
348
|
+
if (!(cause instanceof SharedTransportError)) throw cause;
|
|
349
|
+
throw new AuthyonError(0, { code: cause.code, title: cause.message }, { cause: cause.cause });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async parse(response) {
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
let body = {};
|
|
355
|
+
try {
|
|
356
|
+
body = await response.json();
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
throw new AuthyonError(response.status, body, responseMetadata(response));
|
|
360
|
+
}
|
|
361
|
+
if (response.status === 204) return void 0;
|
|
362
|
+
return await response.json();
|
|
363
|
+
}
|
|
364
|
+
async request(path, options = {}) {
|
|
365
|
+
return this.parse(await this.send(path, options));
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// src/client/authyonClient.ts
|
|
69
370
|
var FALLBACK_EXPIRES_IN = 1800;
|
|
70
371
|
function readTokens(raw) {
|
|
71
372
|
const tokens = raw.tokens ?? raw;
|
|
72
373
|
if (!tokens.accessToken || !tokens.refreshToken) {
|
|
73
374
|
throw new AuthyonError(502, {
|
|
74
|
-
code:
|
|
375
|
+
code: ErrorCodes.SessionMalformed,
|
|
75
376
|
title: "Malformed session response",
|
|
76
377
|
detail: "The session response carried no access/refresh token pair."
|
|
77
378
|
});
|
|
@@ -127,7 +428,7 @@ var AuthyonClient = class {
|
|
|
127
428
|
/** GET /auth/sessions — active refresh-token sessions with device/IP data. */
|
|
128
429
|
sessions: () => this.request("/auth/sessions", { bearer: true }),
|
|
129
430
|
/** GET /auth/me/activities — paginated recent account activity for the current user. */
|
|
130
|
-
activities: (params = {}) => this.request(
|
|
431
|
+
activities: (params = {}) => this.request(appendQuery("/auth/me/activities", params), { bearer: true }),
|
|
131
432
|
/**
|
|
132
433
|
* Revokes a single session by id (e.g. one entry from `sessions()`),
|
|
133
434
|
* signing that device out without affecting the current one.
|
|
@@ -183,11 +484,11 @@ var AuthyonClient = class {
|
|
|
183
484
|
/**
|
|
184
485
|
* GET /auth/tenants/{organizationId}/members — paginated list of an
|
|
185
486
|
* organization's members. Consistent with the confirmed-live
|
|
186
|
-
* `
|
|
487
|
+
* `Paged<T>` envelope every other `skip`/`take` endpoint returns
|
|
187
488
|
* (`user.activities()`, `@authyon/server`'s `environment.users.list()`).
|
|
188
489
|
*/
|
|
189
490
|
list: (organizationId, params = {}) => this.request(
|
|
190
|
-
`/auth/tenants/${encodeURIComponent(organizationId)}/members
|
|
491
|
+
appendQuery(`/auth/tenants/${encodeURIComponent(organizationId)}/members`, params),
|
|
191
492
|
{ bearer: true }
|
|
192
493
|
),
|
|
193
494
|
/** POST /auth/tenants/{organizationId}/members — invite a member by e-mail. */
|
|
@@ -280,10 +581,18 @@ var AuthyonClient = class {
|
|
|
280
581
|
if (!options.envKey)
|
|
281
582
|
throw new Error("Authyon: `envKey` is required (pk_live_... / pk_test_...)");
|
|
282
583
|
this.envKey = options.envKey;
|
|
283
|
-
this.
|
|
284
|
-
|
|
584
|
+
this.transport = createSharedTransport({
|
|
585
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
586
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
587
|
+
timeoutMs: options.timeoutMs,
|
|
588
|
+
httpAdapter: options.httpAdapter,
|
|
589
|
+
httpLogger: options.httpLogger,
|
|
590
|
+
fetch: options.fetch
|
|
591
|
+
});
|
|
592
|
+
this.baseUrl = this.transport.baseUrl;
|
|
593
|
+
this.http = new JsonHttpClient(this.transport);
|
|
594
|
+
this.storage = options.storage ?? createDefaultStorage();
|
|
285
595
|
this.autoRefresh = options.autoRefresh ?? true;
|
|
286
|
-
this.fetchImpl = options.fetch ?? fetch.bind(globalThis);
|
|
287
596
|
}
|
|
288
597
|
// ── Session state ────────────────────────────────────────────────────────
|
|
289
598
|
/** Current persisted session, or null when signed out. */
|
|
@@ -291,7 +600,13 @@ var AuthyonClient = class {
|
|
|
291
600
|
return this.storage.get();
|
|
292
601
|
}
|
|
293
602
|
isAuthenticated() {
|
|
294
|
-
return this.
|
|
603
|
+
return this.getAuthState() === "authenticated";
|
|
604
|
+
}
|
|
605
|
+
/** Synchronous snapshot of the locally available authentication state. */
|
|
606
|
+
getAuthState() {
|
|
607
|
+
const session = this.getSession();
|
|
608
|
+
if (!session) return "signed_out";
|
|
609
|
+
return Date.now() < session.expiresAt ? "authenticated" : "expired";
|
|
295
610
|
}
|
|
296
611
|
/**
|
|
297
612
|
* Returns a valid access token, refreshing it transparently when it is
|
|
@@ -300,15 +615,41 @@ var AuthyonClient = class {
|
|
|
300
615
|
async getAccessToken() {
|
|
301
616
|
const session = this.getSession();
|
|
302
617
|
if (!session) return null;
|
|
303
|
-
if (this.autoRefresh && Date.now() >= session.expiresAt -
|
|
618
|
+
if (this.autoRefresh && Date.now() >= session.expiresAt - DEFAULT_EXPIRY_SKEW_MS) {
|
|
304
619
|
try {
|
|
305
620
|
return (await this.refresh()).accessToken;
|
|
306
|
-
} catch {
|
|
307
|
-
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (error instanceof AuthyonError && (error.status === 401 || error.status === 403)) {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
throw error;
|
|
308
626
|
}
|
|
309
627
|
}
|
|
310
628
|
return session.accessToken;
|
|
311
629
|
}
|
|
630
|
+
/**
|
|
631
|
+
* Refreshes when needed, validates the server-side session through `GET /auth/me`,
|
|
632
|
+
* and stores the fresh user profile. Returns null when the session is no longer valid.
|
|
633
|
+
*/
|
|
634
|
+
async validateSession() {
|
|
635
|
+
const accessToken = await this.getAccessToken();
|
|
636
|
+
if (!accessToken) return null;
|
|
637
|
+
try {
|
|
638
|
+
const user = await this.user.me();
|
|
639
|
+
const current = this.getSession();
|
|
640
|
+
if (!current) return null;
|
|
641
|
+
const session = { ...current, user };
|
|
642
|
+
this.storage.set(session);
|
|
643
|
+
this.emit({ type: "session_validated", session });
|
|
644
|
+
return session;
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (error instanceof AuthyonError && (error.status === 401 || error.status === 403)) {
|
|
647
|
+
this.clearSession();
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
throw error;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
312
653
|
/** Subscribe to sign-in / refresh / sign-out events. Returns an unsubscribe fn. */
|
|
313
654
|
onAuthStateChange(listener) {
|
|
314
655
|
this.listeners.add(listener);
|
|
@@ -354,38 +695,26 @@ var AuthyonClient = class {
|
|
|
354
695
|
"X-Authyon-Environment": this.envKey,
|
|
355
696
|
...options.headers
|
|
356
697
|
};
|
|
357
|
-
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
358
698
|
if (options.bearer) {
|
|
359
699
|
const token = await this.getAccessToken();
|
|
360
700
|
if (!token)
|
|
361
|
-
throw new AuthyonError(401, {
|
|
701
|
+
throw new AuthyonError(401, {
|
|
702
|
+
code: ErrorCodes.NotAuthenticated,
|
|
703
|
+
title: "Not authenticated"
|
|
704
|
+
});
|
|
362
705
|
headers.Authorization = `Bearer ${token}`;
|
|
363
706
|
}
|
|
364
|
-
const response = await this.
|
|
365
|
-
method: options.method ?? "GET",
|
|
366
|
-
headers,
|
|
367
|
-
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
368
|
-
});
|
|
707
|
+
const response = await this.http.send(path, { ...options, headers });
|
|
369
708
|
if (response.status === 401 && options.bearer && this.autoRefresh && !isRetry && this.getSession()) {
|
|
370
709
|
try {
|
|
371
710
|
await this.refresh();
|
|
372
711
|
} catch {
|
|
373
712
|
this.clearSession();
|
|
374
|
-
|
|
713
|
+
return this.http.parse(response);
|
|
375
714
|
}
|
|
376
715
|
return this.request(path, options, true);
|
|
377
716
|
}
|
|
378
|
-
|
|
379
|
-
if (response.status === 204) return void 0;
|
|
380
|
-
return await response.json();
|
|
381
|
-
}
|
|
382
|
-
async toError(response) {
|
|
383
|
-
let body = {};
|
|
384
|
-
try {
|
|
385
|
-
body = await response.json();
|
|
386
|
-
} catch {
|
|
387
|
-
}
|
|
388
|
-
return new AuthyonError(response.status, body);
|
|
717
|
+
return this.http.parse(response);
|
|
389
718
|
}
|
|
390
719
|
// ── Auth flows ───────────────────────────────────────────────────────────
|
|
391
720
|
/** POST /auth/register — creates a new user (rate-limited to 20/hour per IP). */
|
|
@@ -421,7 +750,10 @@ var AuthyonClient = class {
|
|
|
421
750
|
if (this.refreshInFlight) return this.refreshInFlight;
|
|
422
751
|
const current = this.getSession();
|
|
423
752
|
if (!current)
|
|
424
|
-
throw new AuthyonError(401, {
|
|
753
|
+
throw new AuthyonError(401, {
|
|
754
|
+
code: ErrorCodes.NotAuthenticated,
|
|
755
|
+
title: "Not authenticated"
|
|
756
|
+
});
|
|
425
757
|
this.refreshInFlight = this.request("/auth/refresh", {
|
|
426
758
|
method: "POST",
|
|
427
759
|
body: { refreshToken: current.refreshToken }
|
|
@@ -471,6 +803,8 @@ var AuthyonClient = class {
|
|
|
471
803
|
* A browser app has no client secret to present, so this will fail from
|
|
472
804
|
* `@authyon/auth` in practice; call it from your backend via
|
|
473
805
|
* `@authyon/server` instead.
|
|
806
|
+
*
|
|
807
|
+
* @deprecated Use `@authyon/server.introspect()` from a trusted backend.
|
|
474
808
|
*/
|
|
475
809
|
async introspect(token) {
|
|
476
810
|
const accessToken = token ?? await this.getAccessToken();
|
|
@@ -480,6 +814,8 @@ var AuthyonClient = class {
|
|
|
480
814
|
* POST /auth/validate — recommended: cross-checks DB state, catches
|
|
481
815
|
* revocation immediately. Same caller-authentication requirement (and
|
|
482
816
|
* the same practical limitation from the browser) as `introspect()`.
|
|
817
|
+
*
|
|
818
|
+
* @deprecated Use `@authyon/server.validate()` from a trusted backend.
|
|
483
819
|
*/
|
|
484
820
|
async validate(token) {
|
|
485
821
|
const accessToken = token ?? await this.getAccessToken();
|
|
@@ -502,19 +838,56 @@ function normalizeUser(raw) {
|
|
|
502
838
|
function createClient(options) {
|
|
503
839
|
return new AuthyonClient(options);
|
|
504
840
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
841
|
+
|
|
842
|
+
// src/client/authyonClientBuilder.ts
|
|
843
|
+
var AuthyonClientBuilder = class {
|
|
844
|
+
constructor(envKey) {
|
|
845
|
+
this.options = { envKey };
|
|
509
846
|
}
|
|
510
|
-
|
|
511
|
-
|
|
847
|
+
withBaseUrl(baseUrl, allowInsecureHttp = false) {
|
|
848
|
+
this.options.baseUrl = baseUrl;
|
|
849
|
+
this.options.allowInsecureHttp = allowInsecureHttp;
|
|
850
|
+
return this;
|
|
851
|
+
}
|
|
852
|
+
withStorage(storage) {
|
|
853
|
+
this.options.storage = storage;
|
|
854
|
+
return this;
|
|
855
|
+
}
|
|
856
|
+
withAutomaticRefresh(enabled = true) {
|
|
857
|
+
this.options.autoRefresh = enabled;
|
|
858
|
+
return this;
|
|
859
|
+
}
|
|
860
|
+
withTimeout(timeoutMs) {
|
|
861
|
+
this.options.timeoutMs = timeoutMs;
|
|
862
|
+
return this;
|
|
863
|
+
}
|
|
864
|
+
withHttpAdapter(httpAdapter) {
|
|
865
|
+
this.options.httpAdapter = httpAdapter;
|
|
866
|
+
return this;
|
|
867
|
+
}
|
|
868
|
+
withHttpLogger(httpLogger) {
|
|
869
|
+
this.options.httpLogger = httpLogger;
|
|
870
|
+
return this;
|
|
871
|
+
}
|
|
872
|
+
build() {
|
|
873
|
+
return new AuthyonClient({ ...this.options });
|
|
874
|
+
}
|
|
875
|
+
};
|
|
512
876
|
export {
|
|
877
|
+
AuthyonAbility,
|
|
878
|
+
AuthyonAbilityBuilder,
|
|
513
879
|
AuthyonClient,
|
|
880
|
+
AuthyonClientBuilder,
|
|
514
881
|
AuthyonError,
|
|
882
|
+
AuthyonSessionController,
|
|
515
883
|
ErrorCodes,
|
|
884
|
+
FetchHttpAdapter,
|
|
885
|
+
LoggingHttpAdapter,
|
|
886
|
+
createAuthyonAbility,
|
|
887
|
+
createAuthyonRules,
|
|
516
888
|
createClient,
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
889
|
+
createDefaultStorage,
|
|
890
|
+
createLocalStorage,
|
|
891
|
+
createMemoryStorage,
|
|
892
|
+
hasPermission
|
|
520
893
|
};
|