@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.
@@ -0,0 +1,323 @@
1
+ // src/session/sessionController.ts
2
+ var SERVER_SNAPSHOT = {
3
+ // The server cannot inspect browser storage. Reporting unauthenticated here
4
+ // makes guards redirect during hydration before a persisted session can be
5
+ // restored and validated on the client.
6
+ status: "validating",
7
+ session: null,
8
+ user: null,
9
+ error: null
10
+ };
11
+ var AuthyonSessionController = class {
12
+ constructor(client, options = {}) {
13
+ this.client = client;
14
+ this.listeners = /* @__PURE__ */ new Set();
15
+ this.getSnapshot = () => this.snapshot;
16
+ this.getServerSnapshot = () => SERVER_SNAPSHOT;
17
+ this.subscribe = (listener) => {
18
+ this.listeners.add(listener);
19
+ return () => this.listeners.delete(listener);
20
+ };
21
+ this.refreshAheadMs = options.refreshAheadMs ?? 3e4;
22
+ if (!Number.isFinite(this.refreshAheadMs) || this.refreshAheadMs < 0) {
23
+ throw new Error("Authyon: `refreshAheadMs` must be a non-negative finite number");
24
+ }
25
+ const session = client.getSession();
26
+ this.snapshot = {
27
+ status: session ? "validating" : "unauthenticated",
28
+ session,
29
+ user: session?.user ?? null,
30
+ error: null
31
+ };
32
+ }
33
+ start() {
34
+ if (!this.unsubscribeAuth) {
35
+ this.unsubscribeAuth = this.client.onAuthStateChange((event) => {
36
+ if (event.type === "signed_out") {
37
+ this.cancelRefresh();
38
+ this.setSnapshot({
39
+ status: "unauthenticated",
40
+ session: null,
41
+ user: null,
42
+ error: null
43
+ });
44
+ return;
45
+ }
46
+ if (event.type === "session_validated") {
47
+ this.acceptSession(event.session);
48
+ return;
49
+ }
50
+ this.setSnapshot({
51
+ status: "validating",
52
+ session: event.session,
53
+ user: event.session.user ?? null,
54
+ error: null
55
+ });
56
+ void this.validate();
57
+ });
58
+ }
59
+ void this.validate();
60
+ return () => this.stop();
61
+ }
62
+ stop() {
63
+ this.unsubscribeAuth?.();
64
+ this.unsubscribeAuth = void 0;
65
+ this.cancelRefresh();
66
+ }
67
+ validate() {
68
+ if (this.validation) return this.validation;
69
+ const localSession = this.client.getSession();
70
+ if (!localSession) {
71
+ this.setSnapshot({
72
+ status: "unauthenticated",
73
+ session: null,
74
+ user: null,
75
+ error: null
76
+ });
77
+ return Promise.resolve(this.snapshot);
78
+ }
79
+ this.setSnapshot({ ...this.snapshot, status: "validating", error: null });
80
+ this.validation = this.client.validateSession().then((session) => {
81
+ if (session) this.acceptSession(session);
82
+ else {
83
+ this.setSnapshot({
84
+ status: "unauthenticated",
85
+ session: null,
86
+ user: null,
87
+ error: null
88
+ });
89
+ }
90
+ return this.snapshot;
91
+ }).catch((error) => {
92
+ this.setSnapshot({ ...this.snapshot, status: "error", error });
93
+ return this.snapshot;
94
+ }).finally(() => {
95
+ this.validation = void 0;
96
+ });
97
+ return this.validation;
98
+ }
99
+ async refreshNow() {
100
+ if (!this.client.getSession()) return this.validate();
101
+ try {
102
+ await this.client.refresh();
103
+ } catch (error) {
104
+ if (!this.client.getSession()) return this.validate();
105
+ this.setSnapshot({ ...this.snapshot, status: "error", error });
106
+ return this.snapshot;
107
+ }
108
+ return this.validate();
109
+ }
110
+ acceptSession(session) {
111
+ this.setSnapshot({
112
+ status: "authenticated",
113
+ session,
114
+ user: session.user ?? null,
115
+ error: null
116
+ });
117
+ this.scheduleRefresh(session);
118
+ }
119
+ scheduleRefresh(session) {
120
+ this.cancelRefresh();
121
+ const delay = Math.max(1e3, session.expiresAt - Date.now() - this.refreshAheadMs);
122
+ this.refreshTimer = setTimeout(() => void this.refreshNow(), delay);
123
+ }
124
+ cancelRefresh() {
125
+ if (this.refreshTimer !== void 0) clearTimeout(this.refreshTimer);
126
+ this.refreshTimer = void 0;
127
+ }
128
+ setSnapshot(snapshot) {
129
+ this.snapshot = snapshot;
130
+ for (const listener of this.listeners) listener();
131
+ }
132
+ };
133
+
134
+ // ../../internal/core/authorization/ability.ts
135
+ var AuthyonAbility = class {
136
+ constructor(rules = [], detectSubjectType = defaultSubjectType) {
137
+ this.detectSubjectType = detectSubjectType;
138
+ this.listeners = /* @__PURE__ */ new Set();
139
+ this.currentRules = rules.map(cloneRule);
140
+ }
141
+ get rules() {
142
+ return this.currentRules;
143
+ }
144
+ can(action, subject, field) {
145
+ const subjectType = typeof subject === "string" ? subject : this.detectSubjectType(subject);
146
+ for (let index = this.currentRules.length - 1; index >= 0; index -= 1) {
147
+ const rule = this.currentRules[index];
148
+ if (!matchesToken(rule.action, action, "manage")) continue;
149
+ if (!matchesToken(rule.subject, subjectType, "all")) continue;
150
+ if (field && rule.fields && !rule.fields.some((value) => matchesField(value, field)))
151
+ continue;
152
+ if (rule.conditions) {
153
+ if (typeof subject === "string" || !matchesConditions(subject, rule.conditions)) continue;
154
+ }
155
+ return !rule.inverted;
156
+ }
157
+ return false;
158
+ }
159
+ cannot(action, subject, field) {
160
+ return !this.can(action, subject, field);
161
+ }
162
+ rulesFor(action, subject) {
163
+ return this.currentRules.filter(
164
+ (rule) => matchesToken(rule.action, action, "manage") && matchesToken(rule.subject, subject, "all")
165
+ );
166
+ }
167
+ update(rules) {
168
+ this.currentRules = rules.map(cloneRule);
169
+ for (const listener of this.listeners) listener(this.rules);
170
+ }
171
+ on(event, listener) {
172
+ if (event !== "updated") return () => void 0;
173
+ this.listeners.add(listener);
174
+ return () => this.listeners.delete(listener);
175
+ }
176
+ };
177
+ var AuthyonAbilityBuilder = class {
178
+ constructor() {
179
+ this.rules = [];
180
+ }
181
+ can(action, subject, conditions, fields) {
182
+ this.rules.push({ action, subject, conditions, fields });
183
+ return this;
184
+ }
185
+ cannot(action, subject, conditions, fields, reason) {
186
+ this.rules.push({ action, subject, conditions, fields, reason, inverted: true });
187
+ return this;
188
+ }
189
+ build(options = {}) {
190
+ return new AuthyonAbility(this.rules, options.detectSubjectType);
191
+ }
192
+ };
193
+ function createAuthyonAbility(source = {}, options = {}) {
194
+ return new AuthyonAbility(createAuthyonRules(source, options), options.detectSubjectType);
195
+ }
196
+ function hasPermission(source, requiredPermission) {
197
+ const requirement = permissionToRule(requiredPermission);
198
+ if (!requirement || typeof requirement.action !== "string" || typeof requirement.subject !== "string") {
199
+ return false;
200
+ }
201
+ const permissionSource = isPermissionList(source) ? { permissions: source } : source;
202
+ return createAuthyonAbility(permissionSource).can(requirement.action, requirement.subject);
203
+ }
204
+ function isPermissionList(source) {
205
+ return Array.isArray(source);
206
+ }
207
+ function createAuthyonRules(source = {}, options = {}) {
208
+ const permissions = /* @__PURE__ */ new Set([
209
+ ...source.permissions ?? [],
210
+ ...source.scope?.split(/\s+/).filter(Boolean) ?? []
211
+ ]);
212
+ const rules = [...permissions].map(permissionToRule).filter(isAbilityRule);
213
+ for (const role of /* @__PURE__ */ new Set([...source.roles ?? [], ...options.roles ?? []])) {
214
+ rules.push(...(options.roleRules?.[role] ?? []).map(cloneRule));
215
+ }
216
+ rules.push(...(options.rules ?? []).map(cloneRule));
217
+ return rules;
218
+ }
219
+ function permissionToRule(permission) {
220
+ const normalized = permission.trim();
221
+ if (!normalized) return null;
222
+ if (normalized === "*" || normalized === "*:*" || normalized === "all:manage") {
223
+ return { action: "manage", subject: "all" };
224
+ }
225
+ const separator = normalized.lastIndexOf(":");
226
+ if (separator <= 0 || separator === normalized.length - 1) return null;
227
+ const subject = normalized.slice(0, separator);
228
+ const action = normalized.slice(separator + 1);
229
+ return {
230
+ action: action === "*" ? "manage" : action,
231
+ subject: subject === "*" ? "all" : subject
232
+ };
233
+ }
234
+ function matchesToken(value, expected, wildcard) {
235
+ return (Array.isArray(value) ? value : [value]).some(
236
+ (candidate) => candidate === expected || candidate === wildcard || candidate === "*" || matchesSegments(candidate, expected)
237
+ );
238
+ }
239
+ function matchesSegments(pattern, value) {
240
+ const patternSegments = pattern.split(":");
241
+ const valueSegments = value.split(":");
242
+ return patternSegments.length === valueSegments.length && patternSegments.every((segment, index) => segment === "*" || segment === valueSegments[index]);
243
+ }
244
+ function matchesField(pattern, field) {
245
+ if (pattern === "*" || pattern === field) return true;
246
+ return pattern.endsWith(".*") && field.startsWith(pattern.slice(0, -1));
247
+ }
248
+ function matchesConditions(subject, conditions) {
249
+ return Object.entries(conditions).every(([path, expected]) => {
250
+ if (path === "$and" && Array.isArray(expected)) {
251
+ return expected.every(
252
+ (condition) => matchesConditions(subject, condition)
253
+ );
254
+ }
255
+ if (path === "$or" && Array.isArray(expected)) {
256
+ return expected.some(
257
+ (condition) => matchesConditions(subject, condition)
258
+ );
259
+ }
260
+ return matchesValue(readPath(subject, path), expected);
261
+ });
262
+ }
263
+ function matchesValue(actual, expected) {
264
+ if (!isRecord(expected) || !Object.keys(expected).some((key) => key.startsWith("$"))) {
265
+ return isRecord(expected) && isRecord(actual) ? matchesConditions(actual, expected) : Object.is(actual, expected);
266
+ }
267
+ return Object.entries(expected).every(([operator, operand]) => {
268
+ switch (operator) {
269
+ case "$eq":
270
+ return Object.is(actual, operand);
271
+ case "$ne":
272
+ return !Object.is(actual, operand);
273
+ case "$in":
274
+ return Array.isArray(operand) && operand.some((value) => Object.is(actual, value));
275
+ case "$nin":
276
+ return Array.isArray(operand) && !operand.some((value) => Object.is(actual, value));
277
+ case "$gt":
278
+ return typeof actual === "number" && typeof operand === "number" && actual > operand;
279
+ case "$gte":
280
+ return typeof actual === "number" && typeof operand === "number" && actual >= operand;
281
+ case "$lt":
282
+ return typeof actual === "number" && typeof operand === "number" && actual < operand;
283
+ case "$lte":
284
+ return typeof actual === "number" && typeof operand === "number" && actual <= operand;
285
+ case "$exists":
286
+ return operand ? actual !== void 0 : actual === void 0;
287
+ default:
288
+ return false;
289
+ }
290
+ });
291
+ }
292
+ function readPath(value, path) {
293
+ return path.split(".").reduce((current, part) => isRecord(current) ? current[part] : void 0, value);
294
+ }
295
+ function defaultSubjectType(subject) {
296
+ const explicit = subject.__type ?? subject.type ?? subject.kind;
297
+ if (typeof explicit === "string") return explicit;
298
+ const constructorName = subject.constructor?.name;
299
+ return typeof constructorName === "string" ? constructorName : "Object";
300
+ }
301
+ function cloneRule(rule) {
302
+ return {
303
+ ...rule,
304
+ action: Array.isArray(rule.action) ? [...rule.action] : rule.action,
305
+ subject: Array.isArray(rule.subject) ? [...rule.subject] : rule.subject,
306
+ fields: rule.fields ? [...rule.fields] : void 0
307
+ };
308
+ }
309
+ function isRecord(value) {
310
+ return typeof value === "object" && value !== null && !Array.isArray(value);
311
+ }
312
+ function isAbilityRule(value) {
313
+ return value !== null;
314
+ }
315
+
316
+ export {
317
+ AuthyonSessionController,
318
+ AuthyonAbility,
319
+ AuthyonAbilityBuilder,
320
+ createAuthyonAbility,
321
+ hasPermission,
322
+ createAuthyonRules
323
+ };