@authyon/auth 0.2.0-beta.0 → 0.2.0-beta.11
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 +178 -15
- package/dist/ability-DhtyQ5wv.d.cts +643 -0
- package/dist/ability-DhtyQ5wv.d.ts +643 -0
- package/dist/chunk-3XJY6BCI.js +329 -0
- package/dist/index.cjs +790 -62
- package/dist/index.d.cts +58 -466
- package/dist/index.d.ts +58 -466
- package/dist/index.js +468 -60
- 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.cjs
CHANGED
|
@@ -20,39 +20,142 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
AuthyonAbility: () => AuthyonAbility,
|
|
24
|
+
AuthyonAbilityBuilder: () => AuthyonAbilityBuilder,
|
|
23
25
|
AuthyonClient: () => AuthyonClient,
|
|
26
|
+
AuthyonClientBuilder: () => AuthyonClientBuilder,
|
|
24
27
|
AuthyonError: () => AuthyonError,
|
|
28
|
+
AuthyonSessionController: () => AuthyonSessionController,
|
|
25
29
|
ErrorCodes: () => ErrorCodes,
|
|
30
|
+
FetchHttpAdapter: () => FetchHttpAdapter,
|
|
31
|
+
LoggingHttpAdapter: () => LoggingHttpAdapter,
|
|
32
|
+
createAuthyonAbility: () => createAuthyonAbility,
|
|
33
|
+
createAuthyonRules: () => createAuthyonRules,
|
|
26
34
|
createClient: () => createClient,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
createDefaultStorage: () => createDefaultStorage,
|
|
36
|
+
createLocalStorage: () => createLocalStorage,
|
|
37
|
+
createMemoryStorage: () => createMemoryStorage,
|
|
38
|
+
hasPermission: () => hasPermission,
|
|
39
|
+
hasPermissionGroup: () => hasPermissionGroup
|
|
30
40
|
});
|
|
31
41
|
module.exports = __toCommonJS(index_exports);
|
|
32
42
|
|
|
33
|
-
//
|
|
43
|
+
// ../../internal/core/errors/authyonError.ts
|
|
44
|
+
var ErrorCodes = {
|
|
45
|
+
Unknown: "unknown",
|
|
46
|
+
NetworkError: "request.network_error",
|
|
47
|
+
Timeout: "request.timeout",
|
|
48
|
+
SessionMalformed: "session.malformed",
|
|
49
|
+
NotAuthenticated: "auth.not_authenticated",
|
|
50
|
+
InvalidToken: "auth.invalid_token",
|
|
51
|
+
MissingToken: "auth.missing_token",
|
|
52
|
+
EmailTaken: "user.email_taken",
|
|
53
|
+
PasswordWeak: "user.password_weak",
|
|
54
|
+
PasswordPwned: "user.password_pwned",
|
|
55
|
+
RateLimited: "rate_limited"
|
|
56
|
+
};
|
|
34
57
|
var AuthyonError = class extends Error {
|
|
35
|
-
constructor(status, body) {
|
|
58
|
+
constructor(status, body, options = {}) {
|
|
36
59
|
super(body.detail ?? body.title ?? `Authyon request failed with status ${status}`);
|
|
37
60
|
this.name = "AuthyonError";
|
|
38
61
|
this.status = status;
|
|
39
62
|
this.code = body.code ?? "unknown";
|
|
40
63
|
this.title = body.title ?? "Error";
|
|
41
64
|
this.detail = body.detail;
|
|
65
|
+
this.requestId = options.requestId;
|
|
66
|
+
this.retryAfter = options.retryAfter;
|
|
67
|
+
this.cause = options.cause;
|
|
42
68
|
}
|
|
43
69
|
is(code) {
|
|
44
70
|
return this.code === code;
|
|
45
71
|
}
|
|
72
|
+
isAny(...codes) {
|
|
73
|
+
return codes.includes(this.code);
|
|
74
|
+
}
|
|
75
|
+
hasPrefix(prefix) {
|
|
76
|
+
return this.code.startsWith(prefix);
|
|
77
|
+
}
|
|
78
|
+
isStatus(...statuses) {
|
|
79
|
+
return statuses.includes(this.status);
|
|
80
|
+
}
|
|
81
|
+
/** Stable interpretation for UI decisions, retries, telemetry and support flows. */
|
|
82
|
+
interpret() {
|
|
83
|
+
const category = classifyError(this.status, this.code);
|
|
84
|
+
return {
|
|
85
|
+
category,
|
|
86
|
+
action: actionFor(category),
|
|
87
|
+
retryable: isRetryable(category),
|
|
88
|
+
...this.retryAfter !== void 0 ? { retryAfter: this.retryAfter } : {},
|
|
89
|
+
...this.requestId !== void 0 ? { requestId: this.requestId } : {}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
get category() {
|
|
93
|
+
return this.interpret().category;
|
|
94
|
+
}
|
|
95
|
+
get retryable() {
|
|
96
|
+
return this.interpret().retryable;
|
|
97
|
+
}
|
|
98
|
+
toJSON() {
|
|
99
|
+
return {
|
|
100
|
+
name: this.name,
|
|
101
|
+
message: this.message,
|
|
102
|
+
status: this.status,
|
|
103
|
+
code: this.code,
|
|
104
|
+
title: this.title,
|
|
105
|
+
detail: this.detail,
|
|
106
|
+
requestId: this.requestId,
|
|
107
|
+
retryAfter: this.retryAfter,
|
|
108
|
+
...this.interpret()
|
|
109
|
+
};
|
|
110
|
+
}
|
|
46
111
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
112
|
+
function classifyError(status, code) {
|
|
113
|
+
if (code === ErrorCodes.NetworkError) return "network";
|
|
114
|
+
if (code === ErrorCodes.Timeout || status === 408) return "timeout";
|
|
115
|
+
if (code === ErrorCodes.RateLimited || status === 429) return "rate_limit";
|
|
116
|
+
if (code === ErrorCodes.EmailTaken || status === 409) return "conflict";
|
|
117
|
+
if (code === ErrorCodes.PasswordWeak || code === ErrorCodes.PasswordPwned) return "validation";
|
|
118
|
+
if (code === ErrorCodes.SessionMalformed) return "server";
|
|
119
|
+
if (code.startsWith("auth.") || status === 401) return "authentication";
|
|
120
|
+
if (status === 403) return "authorization";
|
|
121
|
+
if (status === 404) return "not_found";
|
|
122
|
+
if (status === 400 || status === 422) return "validation";
|
|
123
|
+
if (status >= 500) return "server";
|
|
124
|
+
return "unknown";
|
|
125
|
+
}
|
|
126
|
+
function actionFor(category) {
|
|
127
|
+
switch (category) {
|
|
128
|
+
case "network":
|
|
129
|
+
case "timeout":
|
|
130
|
+
case "rate_limit":
|
|
131
|
+
case "server":
|
|
132
|
+
return "retry";
|
|
133
|
+
case "authentication":
|
|
134
|
+
return "reauthenticate";
|
|
135
|
+
case "authorization":
|
|
136
|
+
return "request_access";
|
|
137
|
+
case "validation":
|
|
138
|
+
return "fix_input";
|
|
139
|
+
case "not_found":
|
|
140
|
+
return "not_found";
|
|
141
|
+
case "conflict":
|
|
142
|
+
return "resolve_conflict";
|
|
143
|
+
case "unknown":
|
|
144
|
+
return "contact_support";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function isRetryable(category) {
|
|
148
|
+
return category === "network" || category === "timeout" || category === "rate_limit" || category === "server";
|
|
149
|
+
}
|
|
52
150
|
|
|
53
|
-
// src/storage.ts
|
|
151
|
+
// src/session/storage.ts
|
|
54
152
|
var STORAGE_KEY = "authyon.session";
|
|
55
|
-
function
|
|
153
|
+
function isSession(value) {
|
|
154
|
+
if (!value || typeof value !== "object") return false;
|
|
155
|
+
const candidate = value;
|
|
156
|
+
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);
|
|
157
|
+
}
|
|
158
|
+
function createMemoryStorage() {
|
|
56
159
|
let session = null;
|
|
57
160
|
return {
|
|
58
161
|
get: () => session,
|
|
@@ -64,12 +167,16 @@ function memoryStorage() {
|
|
|
64
167
|
}
|
|
65
168
|
};
|
|
66
169
|
}
|
|
67
|
-
function
|
|
170
|
+
function createLocalStorage(key = STORAGE_KEY) {
|
|
68
171
|
return {
|
|
69
172
|
get() {
|
|
70
173
|
try {
|
|
71
174
|
const raw = window.localStorage.getItem(key);
|
|
72
|
-
|
|
175
|
+
if (!raw) return null;
|
|
176
|
+
const parsed = JSON.parse(raw);
|
|
177
|
+
if (isSession(parsed)) return parsed;
|
|
178
|
+
window.localStorage.removeItem(key);
|
|
179
|
+
return null;
|
|
73
180
|
} catch {
|
|
74
181
|
return null;
|
|
75
182
|
}
|
|
@@ -88,22 +195,244 @@ function localStorageAdapter(key = STORAGE_KEY) {
|
|
|
88
195
|
}
|
|
89
196
|
};
|
|
90
197
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
return localStorageAdapter();
|
|
94
|
-
}
|
|
95
|
-
return memoryStorage();
|
|
198
|
+
function createDefaultStorage() {
|
|
199
|
+
return createMemoryStorage();
|
|
96
200
|
}
|
|
97
201
|
|
|
98
|
-
//
|
|
202
|
+
// ../../internal/core/config/defaults.ts
|
|
99
203
|
var DEFAULT_BASE_URL = "https://api.authyon.com";
|
|
100
|
-
var
|
|
204
|
+
var DEFAULT_EXPIRY_SKEW_MS = 3e4;
|
|
205
|
+
|
|
206
|
+
// ../../internal/core/http/query.ts
|
|
207
|
+
function appendQuery(path, params) {
|
|
208
|
+
if (!params) return path;
|
|
209
|
+
const query = new URLSearchParams();
|
|
210
|
+
for (const [key, value2] of Object.entries(params)) {
|
|
211
|
+
if (value2 !== void 0) query.set(key, String(value2));
|
|
212
|
+
}
|
|
213
|
+
const value = query.toString();
|
|
214
|
+
return value ? `${path}${path.includes("?") ? "&" : "?"}${value}` : path;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ../../internal/core/http/httpAdapter.ts
|
|
218
|
+
var FetchHttpAdapter = class {
|
|
219
|
+
constructor(fetchImpl = fetch.bind(globalThis)) {
|
|
220
|
+
this.fetchImpl = fetchImpl;
|
|
221
|
+
}
|
|
222
|
+
request(request) {
|
|
223
|
+
return this.fetchImpl(request.url, {
|
|
224
|
+
method: request.method,
|
|
225
|
+
headers: request.headers,
|
|
226
|
+
body: request.body,
|
|
227
|
+
signal: request.signal,
|
|
228
|
+
redirect: "error"
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
var LoggingHttpAdapter = class {
|
|
233
|
+
constructor(adapter, options) {
|
|
234
|
+
this.adapter = adapter;
|
|
235
|
+
this.options = options;
|
|
236
|
+
}
|
|
237
|
+
async request(request) {
|
|
238
|
+
const startedAt = Date.now();
|
|
239
|
+
const eventBase = {
|
|
240
|
+
method: request.method,
|
|
241
|
+
url: sanitizeUrl(request.url)
|
|
242
|
+
};
|
|
243
|
+
this.log({ type: "request", ...eventBase, timestamp: startedAt });
|
|
244
|
+
try {
|
|
245
|
+
const response = await this.adapter.request(request);
|
|
246
|
+
this.log({
|
|
247
|
+
type: "response",
|
|
248
|
+
...eventBase,
|
|
249
|
+
status: response.status,
|
|
250
|
+
durationMs: Date.now() - startedAt,
|
|
251
|
+
requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
|
|
252
|
+
timestamp: Date.now()
|
|
253
|
+
});
|
|
254
|
+
return response;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
this.log({
|
|
257
|
+
type: "error",
|
|
258
|
+
...eventBase,
|
|
259
|
+
errorName: error instanceof Error ? error.name : "UnknownError",
|
|
260
|
+
durationMs: Date.now() - startedAt,
|
|
261
|
+
timestamp: Date.now()
|
|
262
|
+
});
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
log(event) {
|
|
267
|
+
if (!this.options.enabled) return;
|
|
268
|
+
const logger = this.options.logger ?? defaultHttpLogger;
|
|
269
|
+
try {
|
|
270
|
+
logger(event);
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
function defaultHttpLogger(event) {
|
|
276
|
+
console.debug("[Authyon HTTP]", event);
|
|
277
|
+
}
|
|
278
|
+
function sanitizeUrl(value) {
|
|
279
|
+
const url = new URL(value);
|
|
280
|
+
url.username = "";
|
|
281
|
+
url.password = "";
|
|
282
|
+
url.hash = "";
|
|
283
|
+
url.pathname = url.pathname.replace(/(\/platform\/workspace-invites\/)[^/]+/u, "$1REDACTED");
|
|
284
|
+
const queryKeys = /* @__PURE__ */ new Set();
|
|
285
|
+
url.searchParams.forEach((_value, key) => queryKeys.add(key));
|
|
286
|
+
url.search = queryKeys.size > 0 ? [...queryKeys].map((key) => `${encodeURIComponent(key)}=REDACTED`).join("&") : "";
|
|
287
|
+
return url.toString();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ../../internal/core/http/transport.ts
|
|
291
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
292
|
+
var SharedTransportError = class extends Error {
|
|
293
|
+
constructor(code, cause) {
|
|
294
|
+
super(code === "request.timeout" ? "Request timed out" : "Network request failed");
|
|
295
|
+
this.name = "SharedTransportError";
|
|
296
|
+
this.code = code;
|
|
297
|
+
this.cause = cause;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
function createSharedTransport(options) {
|
|
301
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl, options.allowInsecureHttp ?? false);
|
|
302
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
303
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
304
|
+
throw new Error("Authyon: `timeoutMs` must be a non-negative finite number");
|
|
305
|
+
}
|
|
306
|
+
if (options.httpAdapter && options.fetch) {
|
|
307
|
+
throw new Error("Authyon: use either `httpAdapter` or `fetch`, not both");
|
|
308
|
+
}
|
|
309
|
+
const baseAdapter = options.httpAdapter ?? new FetchHttpAdapter(options.fetch);
|
|
310
|
+
const httpAdapter = options.httpLogger ? new LoggingHttpAdapter(baseAdapter, options.httpLogger) : baseAdapter;
|
|
311
|
+
return {
|
|
312
|
+
baseUrl,
|
|
313
|
+
async request(path, init) {
|
|
314
|
+
const controller = new AbortController();
|
|
315
|
+
const timeout = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
316
|
+
try {
|
|
317
|
+
return await httpAdapter.request({
|
|
318
|
+
url: `${baseUrl}${path}`,
|
|
319
|
+
method: init.method ?? "GET",
|
|
320
|
+
headers: normalizeHeaders(init.headers),
|
|
321
|
+
body: init.body,
|
|
322
|
+
signal: controller.signal
|
|
323
|
+
});
|
|
324
|
+
} catch (cause) {
|
|
325
|
+
throw new SharedTransportError(
|
|
326
|
+
controller.signal.aborted ? ErrorCodes.Timeout : ErrorCodes.NetworkError,
|
|
327
|
+
cause
|
|
328
|
+
);
|
|
329
|
+
} finally {
|
|
330
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function normalizeHeaders(headers) {
|
|
336
|
+
const normalized = {};
|
|
337
|
+
new Headers(headers).forEach((value, key) => {
|
|
338
|
+
normalized[key] = value;
|
|
339
|
+
});
|
|
340
|
+
return normalized;
|
|
341
|
+
}
|
|
342
|
+
function responseMetadata(response) {
|
|
343
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
344
|
+
const retryAfter = retryAfterHeader ? Number(retryAfterHeader) : void 0;
|
|
345
|
+
return {
|
|
346
|
+
requestId: response.headers.get("x-request-id") ?? response.headers.get("trace-id") ?? void 0,
|
|
347
|
+
retryAfter: Number.isFinite(retryAfter) ? retryAfter : void 0
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function normalizeBaseUrl(value, allowInsecureHttp) {
|
|
351
|
+
let url;
|
|
352
|
+
try {
|
|
353
|
+
url = new URL(value);
|
|
354
|
+
} catch {
|
|
355
|
+
throw new Error("Authyon: `baseUrl` must be an absolute URL");
|
|
356
|
+
}
|
|
357
|
+
const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
358
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && (loopback || allowInsecureHttp))) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
"Authyon: `baseUrl` must use HTTPS (HTTP is allowed only for loopback or with `allowInsecureHttp`)"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
"Authyon: `baseUrl` cannot contain credentials, query parameters, or fragments"
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return url.toString().replace(/\/+$/, "");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ../../internal/core/http/jsonHttpClient.ts
|
|
372
|
+
var JsonHttpClient = class {
|
|
373
|
+
constructor(transport) {
|
|
374
|
+
this.transport = transport;
|
|
375
|
+
}
|
|
376
|
+
async send(path, options = {}) {
|
|
377
|
+
const headers = { ...options.headers };
|
|
378
|
+
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
379
|
+
try {
|
|
380
|
+
return await this.transport.request(appendQuery(path, options.query), {
|
|
381
|
+
method: options.method ?? "GET",
|
|
382
|
+
headers,
|
|
383
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
384
|
+
});
|
|
385
|
+
} catch (cause) {
|
|
386
|
+
if (!(cause instanceof SharedTransportError)) throw cause;
|
|
387
|
+
throw new AuthyonError(0, { code: cause.code, title: cause.message }, { cause: cause.cause });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async parse(response) {
|
|
391
|
+
if (!response.ok) {
|
|
392
|
+
let body = {};
|
|
393
|
+
try {
|
|
394
|
+
body = await response.json();
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
throw new AuthyonError(response.status, body, responseMetadata(response));
|
|
398
|
+
}
|
|
399
|
+
if (response.status === 204) return void 0;
|
|
400
|
+
return await response.json();
|
|
401
|
+
}
|
|
402
|
+
async request(path, options = {}) {
|
|
403
|
+
return this.parse(await this.send(path, options));
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// ../../internal/core/http/clientIp.ts
|
|
408
|
+
function clientIpHeaders(context) {
|
|
409
|
+
if (!context?.clientIp) return {};
|
|
410
|
+
return { "X-Forwarded-For": normalizeClientIp(context.clientIp) };
|
|
411
|
+
}
|
|
412
|
+
function normalizeClientIp(value) {
|
|
413
|
+
let ip = value.trim();
|
|
414
|
+
if (ip.startsWith("[") && ip.endsWith("]")) ip = ip.slice(1, -1);
|
|
415
|
+
if (!ip || ip.length > 45 || ip.includes(",") || /[\r\n\s]/.test(ip) || !/^[0-9a-f:.]+$/i.test(ip) || !ip.includes(".") && !ip.includes(":")) {
|
|
416
|
+
throw invalidClientIp();
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
new URL(ip.includes(":") ? `http://[${ip}]` : `http://${ip}`);
|
|
420
|
+
} catch {
|
|
421
|
+
throw invalidClientIp();
|
|
422
|
+
}
|
|
423
|
+
return ip;
|
|
424
|
+
}
|
|
425
|
+
function invalidClientIp() {
|
|
426
|
+
return new Error("Authyon: `clientIp` must be a single valid IPv4 or IPv6 address");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/client/authyonClient.ts
|
|
101
430
|
var FALLBACK_EXPIRES_IN = 1800;
|
|
102
431
|
function readTokens(raw) {
|
|
103
432
|
const tokens = raw.tokens ?? raw;
|
|
104
433
|
if (!tokens.accessToken || !tokens.refreshToken) {
|
|
105
434
|
throw new AuthyonError(502, {
|
|
106
|
-
code:
|
|
435
|
+
code: ErrorCodes.SessionMalformed,
|
|
107
436
|
title: "Malformed session response",
|
|
108
437
|
detail: "The session response carried no access/refresh token pair."
|
|
109
438
|
});
|
|
@@ -159,7 +488,7 @@ var AuthyonClient = class {
|
|
|
159
488
|
/** GET /auth/sessions — active refresh-token sessions with device/IP data. */
|
|
160
489
|
sessions: () => this.request("/auth/sessions", { bearer: true }),
|
|
161
490
|
/** GET /auth/me/activities — paginated recent account activity for the current user. */
|
|
162
|
-
activities: (params = {}) => this.request(
|
|
491
|
+
activities: (params = {}) => this.request(appendQuery("/auth/me/activities", params), { bearer: true }),
|
|
163
492
|
/**
|
|
164
493
|
* Revokes a single session by id (e.g. one entry from `sessions()`),
|
|
165
494
|
* signing that device out without affecting the current one.
|
|
@@ -184,8 +513,8 @@ var AuthyonClient = class {
|
|
|
184
513
|
};
|
|
185
514
|
// ── Organization ─────────────────────────────────────────────────────────
|
|
186
515
|
this.organization = {
|
|
187
|
-
/** GET /auth/tenants —
|
|
188
|
-
list: () => this.request("/auth/tenants", { bearer: true }),
|
|
516
|
+
/** GET /auth/tenants — filterable list of organization memberships. */
|
|
517
|
+
list: (params = {}) => this.request(appendQuery("/auth/tenants", params), { bearer: true }),
|
|
189
518
|
/**
|
|
190
519
|
* POST /auth/tenants — creates an organization owned by the signed-in
|
|
191
520
|
* user (only available when self-service organization creation is
|
|
@@ -215,11 +544,11 @@ var AuthyonClient = class {
|
|
|
215
544
|
/**
|
|
216
545
|
* GET /auth/tenants/{organizationId}/members — paginated list of an
|
|
217
546
|
* organization's members. Consistent with the confirmed-live
|
|
218
|
-
* `
|
|
547
|
+
* `Paged<T>` envelope every other `skip`/`take` endpoint returns
|
|
219
548
|
* (`user.activities()`, `@authyon/server`'s `environment.users.list()`).
|
|
220
549
|
*/
|
|
221
550
|
list: (organizationId, params = {}) => this.request(
|
|
222
|
-
`/auth/tenants/${encodeURIComponent(organizationId)}/members
|
|
551
|
+
appendQuery(`/auth/tenants/${encodeURIComponent(organizationId)}/members`, params),
|
|
223
552
|
{ bearer: true }
|
|
224
553
|
),
|
|
225
554
|
/** POST /auth/tenants/{organizationId}/members — invite a member by e-mail. */
|
|
@@ -312,10 +641,19 @@ var AuthyonClient = class {
|
|
|
312
641
|
if (!options.envKey)
|
|
313
642
|
throw new Error("Authyon: `envKey` is required (pk_live_... / pk_test_...)");
|
|
314
643
|
this.envKey = options.envKey;
|
|
315
|
-
this.
|
|
316
|
-
this.
|
|
644
|
+
this.clientIp = options.clientIp;
|
|
645
|
+
this.transport = createSharedTransport({
|
|
646
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
647
|
+
allowInsecureHttp: options.allowInsecureHttp,
|
|
648
|
+
timeoutMs: options.timeoutMs,
|
|
649
|
+
httpAdapter: options.httpAdapter,
|
|
650
|
+
httpLogger: options.httpLogger,
|
|
651
|
+
fetch: options.fetch
|
|
652
|
+
});
|
|
653
|
+
this.baseUrl = this.transport.baseUrl;
|
|
654
|
+
this.http = new JsonHttpClient(this.transport);
|
|
655
|
+
this.storage = options.storage ?? createDefaultStorage();
|
|
317
656
|
this.autoRefresh = options.autoRefresh ?? true;
|
|
318
|
-
this.fetchImpl = options.fetch ?? fetch.bind(globalThis);
|
|
319
657
|
}
|
|
320
658
|
// ── Session state ────────────────────────────────────────────────────────
|
|
321
659
|
/** Current persisted session, or null when signed out. */
|
|
@@ -323,7 +661,13 @@ var AuthyonClient = class {
|
|
|
323
661
|
return this.storage.get();
|
|
324
662
|
}
|
|
325
663
|
isAuthenticated() {
|
|
326
|
-
return this.
|
|
664
|
+
return this.getAuthState() === "authenticated";
|
|
665
|
+
}
|
|
666
|
+
/** Synchronous snapshot of the locally available authentication state. */
|
|
667
|
+
getAuthState() {
|
|
668
|
+
const session = this.getSession();
|
|
669
|
+
if (!session) return "signed_out";
|
|
670
|
+
return Date.now() < session.expiresAt ? "authenticated" : "expired";
|
|
327
671
|
}
|
|
328
672
|
/**
|
|
329
673
|
* Returns a valid access token, refreshing it transparently when it is
|
|
@@ -332,15 +676,41 @@ var AuthyonClient = class {
|
|
|
332
676
|
async getAccessToken() {
|
|
333
677
|
const session = this.getSession();
|
|
334
678
|
if (!session) return null;
|
|
335
|
-
if (this.autoRefresh && Date.now() >= session.expiresAt -
|
|
679
|
+
if (this.autoRefresh && Date.now() >= session.expiresAt - DEFAULT_EXPIRY_SKEW_MS) {
|
|
336
680
|
try {
|
|
337
681
|
return (await this.refresh()).accessToken;
|
|
338
|
-
} catch {
|
|
339
|
-
|
|
682
|
+
} catch (error) {
|
|
683
|
+
if (error instanceof AuthyonError && (error.status === 401 || error.status === 403)) {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
throw error;
|
|
340
687
|
}
|
|
341
688
|
}
|
|
342
689
|
return session.accessToken;
|
|
343
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* Refreshes when needed, validates the server-side session through `GET /auth/me`,
|
|
693
|
+
* and stores the fresh user profile. Returns null when the session is no longer valid.
|
|
694
|
+
*/
|
|
695
|
+
async validateSession() {
|
|
696
|
+
const accessToken = await this.getAccessToken();
|
|
697
|
+
if (!accessToken) return null;
|
|
698
|
+
try {
|
|
699
|
+
const user = await this.user.me();
|
|
700
|
+
const current = this.getSession();
|
|
701
|
+
if (!current) return null;
|
|
702
|
+
const session = { ...current, user };
|
|
703
|
+
this.storage.set(session);
|
|
704
|
+
this.emit({ type: "session_validated", session });
|
|
705
|
+
return session;
|
|
706
|
+
} catch (error) {
|
|
707
|
+
if (error instanceof AuthyonError && (error.status === 401 || error.status === 403)) {
|
|
708
|
+
this.clearSession();
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
throw error;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
344
714
|
/** Subscribe to sign-in / refresh / sign-out events. Returns an unsubscribe fn. */
|
|
345
715
|
onAuthStateChange(listener) {
|
|
346
716
|
this.listeners.add(listener);
|
|
@@ -384,40 +754,29 @@ var AuthyonClient = class {
|
|
|
384
754
|
async request(path, options = {}, isRetry = false) {
|
|
385
755
|
const headers = {
|
|
386
756
|
"X-Authyon-Environment": this.envKey,
|
|
757
|
+
...clientIpHeaders({ clientIp: this.clientIp }),
|
|
387
758
|
...options.headers
|
|
388
759
|
};
|
|
389
|
-
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
390
760
|
if (options.bearer) {
|
|
391
761
|
const token = await this.getAccessToken();
|
|
392
762
|
if (!token)
|
|
393
|
-
throw new AuthyonError(401, {
|
|
763
|
+
throw new AuthyonError(401, {
|
|
764
|
+
code: ErrorCodes.NotAuthenticated,
|
|
765
|
+
title: "Not authenticated"
|
|
766
|
+
});
|
|
394
767
|
headers.Authorization = `Bearer ${token}`;
|
|
395
768
|
}
|
|
396
|
-
const response = await this.
|
|
397
|
-
method: options.method ?? "GET",
|
|
398
|
-
headers,
|
|
399
|
-
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
400
|
-
});
|
|
769
|
+
const response = await this.http.send(path, { ...options, headers });
|
|
401
770
|
if (response.status === 401 && options.bearer && this.autoRefresh && !isRetry && this.getSession()) {
|
|
402
771
|
try {
|
|
403
772
|
await this.refresh();
|
|
404
773
|
} catch {
|
|
405
774
|
this.clearSession();
|
|
406
|
-
|
|
775
|
+
return this.http.parse(response);
|
|
407
776
|
}
|
|
408
777
|
return this.request(path, options, true);
|
|
409
778
|
}
|
|
410
|
-
|
|
411
|
-
if (response.status === 204) return void 0;
|
|
412
|
-
return await response.json();
|
|
413
|
-
}
|
|
414
|
-
async toError(response) {
|
|
415
|
-
let body = {};
|
|
416
|
-
try {
|
|
417
|
-
body = await response.json();
|
|
418
|
-
} catch {
|
|
419
|
-
}
|
|
420
|
-
return new AuthyonError(response.status, body);
|
|
779
|
+
return this.http.parse(response);
|
|
421
780
|
}
|
|
422
781
|
// ── Auth flows ───────────────────────────────────────────────────────────
|
|
423
782
|
/** POST /auth/register — creates a new user (rate-limited to 20/hour per IP). */
|
|
@@ -453,7 +812,10 @@ var AuthyonClient = class {
|
|
|
453
812
|
if (this.refreshInFlight) return this.refreshInFlight;
|
|
454
813
|
const current = this.getSession();
|
|
455
814
|
if (!current)
|
|
456
|
-
throw new AuthyonError(401, {
|
|
815
|
+
throw new AuthyonError(401, {
|
|
816
|
+
code: ErrorCodes.NotAuthenticated,
|
|
817
|
+
title: "Not authenticated"
|
|
818
|
+
});
|
|
457
819
|
this.refreshInFlight = this.request("/auth/refresh", {
|
|
458
820
|
method: "POST",
|
|
459
821
|
body: { refreshToken: current.refreshToken }
|
|
@@ -503,6 +865,8 @@ var AuthyonClient = class {
|
|
|
503
865
|
* A browser app has no client secret to present, so this will fail from
|
|
504
866
|
* `@authyon/auth` in practice; call it from your backend via
|
|
505
867
|
* `@authyon/server` instead.
|
|
868
|
+
*
|
|
869
|
+
* @deprecated Use `@authyon/server.introspect()` from a trusted backend.
|
|
506
870
|
*/
|
|
507
871
|
async introspect(token) {
|
|
508
872
|
const accessToken = token ?? await this.getAccessToken();
|
|
@@ -512,6 +876,8 @@ var AuthyonClient = class {
|
|
|
512
876
|
* POST /auth/validate — recommended: cross-checks DB state, catches
|
|
513
877
|
* revocation immediately. Same caller-authentication requirement (and
|
|
514
878
|
* the same practical limitation from the browser) as `introspect()`.
|
|
879
|
+
*
|
|
880
|
+
* @deprecated Use `@authyon/server.validate()` from a trusted backend.
|
|
515
881
|
*/
|
|
516
882
|
async validate(token) {
|
|
517
883
|
const accessToken = token ?? await this.getAccessToken();
|
|
@@ -534,20 +900,382 @@ function normalizeUser(raw) {
|
|
|
534
900
|
function createClient(options) {
|
|
535
901
|
return new AuthyonClient(options);
|
|
536
902
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
903
|
+
|
|
904
|
+
// src/client/authyonClientBuilder.ts
|
|
905
|
+
var AuthyonClientBuilder = class {
|
|
906
|
+
constructor(envKey) {
|
|
907
|
+
this.options = { envKey };
|
|
908
|
+
}
|
|
909
|
+
withBaseUrl(baseUrl, allowInsecureHttp = false) {
|
|
910
|
+
this.options.baseUrl = baseUrl;
|
|
911
|
+
this.options.allowInsecureHttp = allowInsecureHttp;
|
|
912
|
+
return this;
|
|
913
|
+
}
|
|
914
|
+
withStorage(storage) {
|
|
915
|
+
this.options.storage = storage;
|
|
916
|
+
return this;
|
|
917
|
+
}
|
|
918
|
+
withAutomaticRefresh(enabled = true) {
|
|
919
|
+
this.options.autoRefresh = enabled;
|
|
920
|
+
return this;
|
|
921
|
+
}
|
|
922
|
+
withClientIp(clientIp) {
|
|
923
|
+
this.options.clientIp = clientIp;
|
|
924
|
+
return this;
|
|
925
|
+
}
|
|
926
|
+
withTimeout(timeoutMs) {
|
|
927
|
+
this.options.timeoutMs = timeoutMs;
|
|
928
|
+
return this;
|
|
929
|
+
}
|
|
930
|
+
withHttpAdapter(httpAdapter) {
|
|
931
|
+
this.options.httpAdapter = httpAdapter;
|
|
932
|
+
return this;
|
|
933
|
+
}
|
|
934
|
+
withHttpLogger(httpLogger) {
|
|
935
|
+
this.options.httpLogger = httpLogger;
|
|
936
|
+
return this;
|
|
937
|
+
}
|
|
938
|
+
build() {
|
|
939
|
+
return new AuthyonClient({ ...this.options });
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
// src/session/sessionController.ts
|
|
944
|
+
var SERVER_SNAPSHOT = {
|
|
945
|
+
// The server cannot inspect browser storage. Reporting unauthenticated here
|
|
946
|
+
// makes guards redirect during hydration before a persisted session can be
|
|
947
|
+
// restored and validated on the client.
|
|
948
|
+
status: "validating",
|
|
949
|
+
session: null,
|
|
950
|
+
user: null,
|
|
951
|
+
error: null
|
|
952
|
+
};
|
|
953
|
+
var AuthyonSessionController = class {
|
|
954
|
+
constructor(client, options = {}) {
|
|
955
|
+
this.client = client;
|
|
956
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
957
|
+
this.getSnapshot = () => this.snapshot;
|
|
958
|
+
this.getServerSnapshot = () => SERVER_SNAPSHOT;
|
|
959
|
+
this.subscribe = (listener) => {
|
|
960
|
+
this.listeners.add(listener);
|
|
961
|
+
return () => this.listeners.delete(listener);
|
|
962
|
+
};
|
|
963
|
+
this.refreshAheadMs = options.refreshAheadMs ?? 3e4;
|
|
964
|
+
if (!Number.isFinite(this.refreshAheadMs) || this.refreshAheadMs < 0) {
|
|
965
|
+
throw new Error("Authyon: `refreshAheadMs` must be a non-negative finite number");
|
|
966
|
+
}
|
|
967
|
+
const session = client.getSession();
|
|
968
|
+
this.snapshot = {
|
|
969
|
+
status: session ? "validating" : "unauthenticated",
|
|
970
|
+
session,
|
|
971
|
+
user: session?.user ?? null,
|
|
972
|
+
error: null
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
start() {
|
|
976
|
+
if (!this.unsubscribeAuth) {
|
|
977
|
+
this.unsubscribeAuth = this.client.onAuthStateChange((event) => {
|
|
978
|
+
if (event.type === "signed_out") {
|
|
979
|
+
this.cancelRefresh();
|
|
980
|
+
this.setSnapshot({
|
|
981
|
+
status: "unauthenticated",
|
|
982
|
+
session: null,
|
|
983
|
+
user: null,
|
|
984
|
+
error: null
|
|
985
|
+
});
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (event.type === "session_validated") {
|
|
989
|
+
this.acceptSession(event.session);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
this.setSnapshot({
|
|
993
|
+
status: "validating",
|
|
994
|
+
session: event.session,
|
|
995
|
+
user: event.session.user ?? null,
|
|
996
|
+
error: null
|
|
997
|
+
});
|
|
998
|
+
void this.validate();
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
void this.validate();
|
|
1002
|
+
return () => this.stop();
|
|
1003
|
+
}
|
|
1004
|
+
stop() {
|
|
1005
|
+
this.unsubscribeAuth?.();
|
|
1006
|
+
this.unsubscribeAuth = void 0;
|
|
1007
|
+
this.cancelRefresh();
|
|
1008
|
+
}
|
|
1009
|
+
validate() {
|
|
1010
|
+
if (this.validation) return this.validation;
|
|
1011
|
+
const localSession = this.client.getSession();
|
|
1012
|
+
if (!localSession) {
|
|
1013
|
+
this.setSnapshot({
|
|
1014
|
+
status: "unauthenticated",
|
|
1015
|
+
session: null,
|
|
1016
|
+
user: null,
|
|
1017
|
+
error: null
|
|
1018
|
+
});
|
|
1019
|
+
return Promise.resolve(this.snapshot);
|
|
1020
|
+
}
|
|
1021
|
+
this.setSnapshot({ ...this.snapshot, status: "validating", error: null });
|
|
1022
|
+
this.validation = this.client.validateSession().then((session) => {
|
|
1023
|
+
if (session) this.acceptSession(session);
|
|
1024
|
+
else {
|
|
1025
|
+
this.setSnapshot({
|
|
1026
|
+
status: "unauthenticated",
|
|
1027
|
+
session: null,
|
|
1028
|
+
user: null,
|
|
1029
|
+
error: null
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
return this.snapshot;
|
|
1033
|
+
}).catch((error) => {
|
|
1034
|
+
this.setSnapshot({ ...this.snapshot, status: "error", error });
|
|
1035
|
+
return this.snapshot;
|
|
1036
|
+
}).finally(() => {
|
|
1037
|
+
this.validation = void 0;
|
|
1038
|
+
});
|
|
1039
|
+
return this.validation;
|
|
1040
|
+
}
|
|
1041
|
+
async refreshNow() {
|
|
1042
|
+
if (!this.client.getSession()) return this.validate();
|
|
1043
|
+
try {
|
|
1044
|
+
await this.client.refresh();
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
if (!this.client.getSession()) return this.validate();
|
|
1047
|
+
this.setSnapshot({ ...this.snapshot, status: "error", error });
|
|
1048
|
+
return this.snapshot;
|
|
1049
|
+
}
|
|
1050
|
+
return this.validate();
|
|
1051
|
+
}
|
|
1052
|
+
acceptSession(session) {
|
|
1053
|
+
this.setSnapshot({
|
|
1054
|
+
status: "authenticated",
|
|
1055
|
+
session,
|
|
1056
|
+
user: session.user ?? null,
|
|
1057
|
+
error: null
|
|
1058
|
+
});
|
|
1059
|
+
this.scheduleRefresh(session);
|
|
1060
|
+
}
|
|
1061
|
+
scheduleRefresh(session) {
|
|
1062
|
+
this.cancelRefresh();
|
|
1063
|
+
const delay = Math.max(1e3, session.expiresAt - Date.now() - this.refreshAheadMs);
|
|
1064
|
+
this.refreshTimer = setTimeout(() => void this.refreshNow(), delay);
|
|
1065
|
+
}
|
|
1066
|
+
cancelRefresh() {
|
|
1067
|
+
if (this.refreshTimer !== void 0) clearTimeout(this.refreshTimer);
|
|
1068
|
+
this.refreshTimer = void 0;
|
|
1069
|
+
}
|
|
1070
|
+
setSnapshot(snapshot) {
|
|
1071
|
+
this.snapshot = snapshot;
|
|
1072
|
+
for (const listener of this.listeners) listener();
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
// ../../internal/core/authorization/ability.ts
|
|
1077
|
+
var AuthyonAbility = class {
|
|
1078
|
+
constructor(rules = [], detectSubjectType = defaultSubjectType) {
|
|
1079
|
+
this.detectSubjectType = detectSubjectType;
|
|
1080
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
1081
|
+
this.currentRules = rules.map(cloneRule);
|
|
1082
|
+
}
|
|
1083
|
+
get rules() {
|
|
1084
|
+
return this.currentRules;
|
|
1085
|
+
}
|
|
1086
|
+
can(action, subject, field) {
|
|
1087
|
+
const subjectType = typeof subject === "string" ? subject : this.detectSubjectType(subject);
|
|
1088
|
+
for (let index = this.currentRules.length - 1; index >= 0; index -= 1) {
|
|
1089
|
+
const rule = this.currentRules[index];
|
|
1090
|
+
if (!matchesToken(rule.action, action, "manage")) continue;
|
|
1091
|
+
if (!matchesToken(rule.subject, subjectType, "all")) continue;
|
|
1092
|
+
if (field && rule.fields && !rule.fields.some((value) => matchesField(value, field)))
|
|
1093
|
+
continue;
|
|
1094
|
+
if (rule.conditions) {
|
|
1095
|
+
if (typeof subject === "string" || !matchesConditions(subject, rule.conditions)) continue;
|
|
1096
|
+
}
|
|
1097
|
+
return !rule.inverted;
|
|
1098
|
+
}
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
cannot(action, subject, field) {
|
|
1102
|
+
return !this.can(action, subject, field);
|
|
541
1103
|
}
|
|
542
|
-
|
|
1104
|
+
rulesFor(action, subject) {
|
|
1105
|
+
return this.currentRules.filter(
|
|
1106
|
+
(rule) => matchesToken(rule.action, action, "manage") && matchesToken(rule.subject, subject, "all")
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
update(rules) {
|
|
1110
|
+
this.currentRules = rules.map(cloneRule);
|
|
1111
|
+
for (const listener of this.listeners) listener(this.rules);
|
|
1112
|
+
}
|
|
1113
|
+
on(event, listener) {
|
|
1114
|
+
if (event !== "updated") return () => void 0;
|
|
1115
|
+
this.listeners.add(listener);
|
|
1116
|
+
return () => this.listeners.delete(listener);
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
var AuthyonAbilityBuilder = class {
|
|
1120
|
+
constructor() {
|
|
1121
|
+
this.rules = [];
|
|
1122
|
+
}
|
|
1123
|
+
can(action, subject, conditions, fields) {
|
|
1124
|
+
this.rules.push({ action, subject, conditions, fields });
|
|
1125
|
+
return this;
|
|
1126
|
+
}
|
|
1127
|
+
cannot(action, subject, conditions, fields, reason) {
|
|
1128
|
+
this.rules.push({ action, subject, conditions, fields, reason, inverted: true });
|
|
1129
|
+
return this;
|
|
1130
|
+
}
|
|
1131
|
+
build(options = {}) {
|
|
1132
|
+
return new AuthyonAbility(this.rules, options.detectSubjectType);
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
function createAuthyonAbility(source = {}, options = {}) {
|
|
1136
|
+
return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
|
|
1137
|
+
}
|
|
1138
|
+
function hasPermission(source, requiredPermission) {
|
|
1139
|
+
const requirement = permissionToRule(requiredPermission);
|
|
1140
|
+
if (!requirement || typeof requirement.action !== "string" || typeof requirement.subject !== "string") {
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
const permissionSource = isPermissionList(source) ? { permissions: source } : source;
|
|
1144
|
+
return createAuthyonAbility(permissionSource).can(requirement.action, requirement.subject);
|
|
1145
|
+
}
|
|
1146
|
+
function hasPermissionGroup(source, group) {
|
|
1147
|
+
const hasAll = (group.allOf ?? []).every((permission) => hasPermission(source, permission));
|
|
1148
|
+
const hasAny = !group.anyOf?.length || group.anyOf.some((permission) => hasPermission(source, permission));
|
|
1149
|
+
return hasAll && hasAny;
|
|
1150
|
+
}
|
|
1151
|
+
function isPermissionList(source) {
|
|
1152
|
+
return Array.isArray(source);
|
|
1153
|
+
}
|
|
1154
|
+
function createAuthyonRules(source = {}, options = {}) {
|
|
1155
|
+
const permissions = /* @__PURE__ */ new Set([
|
|
1156
|
+
...source.permissions ?? [],
|
|
1157
|
+
...source.scope?.split(/\s+/).filter(Boolean) ?? []
|
|
1158
|
+
]);
|
|
1159
|
+
const rules = [...permissions].map(permissionToRule).filter(isAbilityRule);
|
|
1160
|
+
for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
|
|
1161
|
+
rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
|
|
1162
|
+
}
|
|
1163
|
+
rules.push(...(options.rules ?? []).map(cloneRule));
|
|
1164
|
+
return rules;
|
|
1165
|
+
}
|
|
1166
|
+
function permissionToRule(permission) {
|
|
1167
|
+
const normalized = permission.trim();
|
|
1168
|
+
if (!normalized) return null;
|
|
1169
|
+
if (normalized === "*" || normalized === "*:*" || normalized === "all:manage") {
|
|
1170
|
+
return { action: "manage", subject: "all" };
|
|
1171
|
+
}
|
|
1172
|
+
const separator = normalized.lastIndexOf(":");
|
|
1173
|
+
if (separator <= 0 || separator === normalized.length - 1) return null;
|
|
1174
|
+
const subject = normalized.slice(0, separator);
|
|
1175
|
+
const action = normalized.slice(separator + 1);
|
|
1176
|
+
return {
|
|
1177
|
+
action: action === "*" ? "manage" : action,
|
|
1178
|
+
subject: subject === "*" ? "all" : subject
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function matchesToken(value, expected, wildcard) {
|
|
1182
|
+
return (Array.isArray(value) ? value : [value]).some(
|
|
1183
|
+
(candidate) => candidate === expected || candidate === wildcard || candidate === "*" || matchesSegments(candidate, expected)
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
function matchesSegments(pattern, value) {
|
|
1187
|
+
const patternSegments = pattern.split(":");
|
|
1188
|
+
const valueSegments = value.split(":");
|
|
1189
|
+
return patternSegments.length === valueSegments.length && patternSegments.every((segment, index) => segment === "*" || segment === valueSegments[index]);
|
|
1190
|
+
}
|
|
1191
|
+
function matchesField(pattern, field) {
|
|
1192
|
+
if (pattern === "*" || pattern === field) return true;
|
|
1193
|
+
return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
|
|
1194
|
+
}
|
|
1195
|
+
function matchesConditions(subject, conditions) {
|
|
1196
|
+
return Object.entries(conditions).every(([path, expected]) => {
|
|
1197
|
+
if (path === "$and" && Array.isArray(expected)) {
|
|
1198
|
+
return expected.every(
|
|
1199
|
+
(condition) => matchesConditions(subject, condition)
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
if (path === "$or" && Array.isArray(expected)) {
|
|
1203
|
+
return expected.some(
|
|
1204
|
+
(condition) => matchesConditions(subject, condition)
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
return matchesValue(readPath(subject, path), expected);
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
function matchesValue(actual, expected) {
|
|
1211
|
+
if (!isRecord(expected) || !Object.keys(expected).some((key) => key.startsWith("$"))) {
|
|
1212
|
+
return isRecord(expected) && isRecord(actual) ? matchesConditions(actual, expected) : Object.is(actual, expected);
|
|
1213
|
+
}
|
|
1214
|
+
return Object.entries(expected).every(([operator, operand]) => {
|
|
1215
|
+
switch (operator) {
|
|
1216
|
+
case "$eq":
|
|
1217
|
+
return Object.is(actual, operand);
|
|
1218
|
+
case "$ne":
|
|
1219
|
+
return !Object.is(actual, operand);
|
|
1220
|
+
case "$in":
|
|
1221
|
+
return Array.isArray(operand) && operand.some((value) => Object.is(actual, value));
|
|
1222
|
+
case "$nin":
|
|
1223
|
+
return Array.isArray(operand) && !operand.some((value) => Object.is(actual, value));
|
|
1224
|
+
case "$gt":
|
|
1225
|
+
return typeof actual === "number" && typeof operand === "number" && actual > operand;
|
|
1226
|
+
case "$gte":
|
|
1227
|
+
return typeof actual === "number" && typeof operand === "number" && actual >= operand;
|
|
1228
|
+
case "$lt":
|
|
1229
|
+
return typeof actual === "number" && typeof operand === "number" && actual < operand;
|
|
1230
|
+
case "$lte":
|
|
1231
|
+
return typeof actual === "number" && typeof operand === "number" && actual <= operand;
|
|
1232
|
+
case "$exists":
|
|
1233
|
+
return operand ? actual !== void 0 : actual === void 0;
|
|
1234
|
+
default:
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
function readPath(value, path) {
|
|
1240
|
+
return path.split(".").reduce((current, part) => isRecord(current) ? current[part] : void 0, value);
|
|
1241
|
+
}
|
|
1242
|
+
function defaultSubjectType(subject) {
|
|
1243
|
+
const explicit = subject.__type ?? subject.type ?? subject.kind;
|
|
1244
|
+
if (typeof explicit === "string") return explicit;
|
|
1245
|
+
const constructorName = subject.constructor?.name;
|
|
1246
|
+
return typeof constructorName === "string" ? constructorName : "Object";
|
|
1247
|
+
}
|
|
1248
|
+
function cloneRule(rule) {
|
|
1249
|
+
return {
|
|
1250
|
+
...rule,
|
|
1251
|
+
action: Array.isArray(rule.action) ? [...rule.action] : rule.action,
|
|
1252
|
+
subject: Array.isArray(rule.subject) ? [...rule.subject] : rule.subject,
|
|
1253
|
+
fields: rule.fields ? [...rule.fields] : void 0
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function isRecord(value) {
|
|
1257
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1258
|
+
}
|
|
1259
|
+
function isAbilityRule(value) {
|
|
1260
|
+
return value !== null;
|
|
543
1261
|
}
|
|
544
1262
|
// Annotate the CommonJS export names for ESM import in node:
|
|
545
1263
|
0 && (module.exports = {
|
|
1264
|
+
AuthyonAbility,
|
|
1265
|
+
AuthyonAbilityBuilder,
|
|
546
1266
|
AuthyonClient,
|
|
1267
|
+
AuthyonClientBuilder,
|
|
547
1268
|
AuthyonError,
|
|
1269
|
+
AuthyonSessionController,
|
|
548
1270
|
ErrorCodes,
|
|
1271
|
+
FetchHttpAdapter,
|
|
1272
|
+
LoggingHttpAdapter,
|
|
1273
|
+
createAuthyonAbility,
|
|
1274
|
+
createAuthyonRules,
|
|
549
1275
|
createClient,
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
1276
|
+
createDefaultStorage,
|
|
1277
|
+
createLocalStorage,
|
|
1278
|
+
createMemoryStorage,
|
|
1279
|
+
hasPermission,
|
|
1280
|
+
hasPermissionGroup
|
|
553
1281
|
});
|