@vertz/ui 0.2.12 → 0.2.13
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/README.md +70 -6
- package/dist/shared/{chunk-bjcpcq5j.js → chunk-2sth83bd.js} +1 -1
- package/dist/shared/{chunk-9e92w0wt.js → chunk-83g4h38e.js} +13 -0
- package/dist/shared/{chunk-2rs8a26p.js → chunk-8hsz5y4a.js} +92 -33
- package/dist/shared/{chunk-55tgkc7s.js → chunk-c30eg6wn.js} +1 -1
- package/dist/shared/{chunk-kg898f92.js → chunk-c9xxsrat.js} +7 -2
- package/dist/shared/{chunk-wn4gv1qd.js → chunk-dksg08fq.js} +1 -1
- package/dist/shared/{chunk-g4rch80a.js → chunk-h89w580h.js} +7 -0
- package/dist/shared/chunk-hw67ckr3.js +1212 -0
- package/dist/shared/{chunk-662f9zrb.js → chunk-j6qyxfdc.js} +7 -7
- package/dist/shared/{chunk-g1gf16fz.js → chunk-mj7b4t40.js} +107 -41
- package/dist/shared/{chunk-18jzqefd.js → chunk-nn9v1zmk.js} +4 -4
- package/dist/src/auth/public.d.ts +303 -0
- package/dist/src/auth/public.js +773 -0
- package/dist/src/css/public.js +22 -0
- package/dist/{form → src/form}/public.js +2 -2
- package/dist/{index.d.ts → src/index.d.ts} +218 -14
- package/dist/{index.js → src/index.js} +79 -229
- package/dist/{internals.d.ts → src/internals.d.ts} +265 -3
- package/dist/{internals.js → src/internals.js} +18 -10
- package/dist/{jsx-runtime → src/jsx-runtime}/index.js +1 -1
- package/dist/{query → src/query}/public.d.ts +3 -1
- package/dist/src/query/public.js +15 -0
- package/dist/{router → src/router}/public.d.ts +25 -4
- package/dist/{router → src/router}/public.js +9 -9
- package/dist/{test → src/test}/index.d.ts +12 -2
- package/dist/{test → src/test}/index.js +4 -4
- package/package.json +31 -25
- package/reactivity.json +67 -0
- package/dist/css/public.js +0 -22
- package/dist/query/public.js +0 -15
- package/dist/shared/chunk-9k2z3jfx.js +0 -528
- /package/dist/{css → src/css}/public.d.ts +0 -0
- /package/dist/{form → src/form}/public.d.ts +0 -0
- /package/dist/{jsx-runtime → src/jsx-runtime}/index.d.ts +0 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import {
|
|
2
|
+
_tryOnCleanup,
|
|
3
|
+
computed,
|
|
4
|
+
createContext,
|
|
5
|
+
signal,
|
|
6
|
+
useContext
|
|
7
|
+
} from "../../shared/chunk-8hsz5y4a.js";
|
|
8
|
+
|
|
9
|
+
// src/auth/access-context.ts
|
|
10
|
+
var AccessContext = createContext(undefined, "@vertz/ui::AccessContext");
|
|
11
|
+
function useAccessContext() {
|
|
12
|
+
const ctx = useContext(AccessContext);
|
|
13
|
+
if (!ctx) {
|
|
14
|
+
throw new Error("useAccessContext must be called within AccessContext.Provider");
|
|
15
|
+
}
|
|
16
|
+
return ctx;
|
|
17
|
+
}
|
|
18
|
+
function createFallbackDenied() {
|
|
19
|
+
return {
|
|
20
|
+
allowed: computed(() => false),
|
|
21
|
+
reasons: computed(() => ["not_authenticated"]),
|
|
22
|
+
reason: computed(() => "not_authenticated"),
|
|
23
|
+
meta: computed(() => {
|
|
24
|
+
return;
|
|
25
|
+
}),
|
|
26
|
+
loading: computed(() => false)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
var __DEV__ = typeof process !== "undefined" && true;
|
|
30
|
+
function can(entitlement, entity) {
|
|
31
|
+
const ctx = useContext(AccessContext);
|
|
32
|
+
if (!ctx) {
|
|
33
|
+
if (__DEV__) {
|
|
34
|
+
console.warn("can() called without AccessContext.Provider — all checks denied");
|
|
35
|
+
}
|
|
36
|
+
return createFallbackDenied();
|
|
37
|
+
}
|
|
38
|
+
const accessData = computed(() => {
|
|
39
|
+
if (entity?.__access?.[entitlement])
|
|
40
|
+
return entity.__access[entitlement];
|
|
41
|
+
const set = ctx.accessSet;
|
|
42
|
+
return set?.entitlements[entitlement] ?? null;
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
allowed: computed(() => accessData.value?.allowed ?? false),
|
|
46
|
+
reasons: computed(() => accessData.value?.reasons ?? []),
|
|
47
|
+
reason: computed(() => accessData.value?.reason),
|
|
48
|
+
meta: computed(() => accessData.value?.meta),
|
|
49
|
+
loading: computed(() => {
|
|
50
|
+
const set = ctx.accessSet;
|
|
51
|
+
if (!set)
|
|
52
|
+
return ctx.loading;
|
|
53
|
+
return false;
|
|
54
|
+
})
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// src/auth/access-event-client.ts
|
|
58
|
+
var BASE_BACKOFF_MS = 1000;
|
|
59
|
+
var MAX_BACKOFF_MS = 30000;
|
|
60
|
+
var JITTER_FACTOR = 0.25;
|
|
61
|
+
function createAccessEventClient(options) {
|
|
62
|
+
const { onEvent, onReconnect } = options;
|
|
63
|
+
let ws = null;
|
|
64
|
+
let reconnectTimer;
|
|
65
|
+
let backoffMs = BASE_BACKOFF_MS;
|
|
66
|
+
let hasConnectedBefore = false;
|
|
67
|
+
let intentionalDisconnect = false;
|
|
68
|
+
let disposed = false;
|
|
69
|
+
function getUrl() {
|
|
70
|
+
if (options.url)
|
|
71
|
+
return options.url;
|
|
72
|
+
if (typeof window !== "undefined") {
|
|
73
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
74
|
+
return `${protocol}//${window.location.host}/api/auth/access-events`;
|
|
75
|
+
}
|
|
76
|
+
return "ws://localhost/api/auth/access-events";
|
|
77
|
+
}
|
|
78
|
+
function applyJitter(delay) {
|
|
79
|
+
const jitter = delay * JITTER_FACTOR;
|
|
80
|
+
return delay - jitter + Math.random() * jitter * 2;
|
|
81
|
+
}
|
|
82
|
+
function scheduleReconnect() {
|
|
83
|
+
if (intentionalDisconnect || disposed)
|
|
84
|
+
return;
|
|
85
|
+
const delay = applyJitter(backoffMs);
|
|
86
|
+
reconnectTimer = setTimeout(() => {
|
|
87
|
+
reconnectTimer = undefined;
|
|
88
|
+
doConnect();
|
|
89
|
+
}, delay);
|
|
90
|
+
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
91
|
+
}
|
|
92
|
+
function clearReconnectTimer() {
|
|
93
|
+
if (reconnectTimer !== undefined) {
|
|
94
|
+
clearTimeout(reconnectTimer);
|
|
95
|
+
reconnectTimer = undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function doConnect() {
|
|
99
|
+
if (disposed)
|
|
100
|
+
return;
|
|
101
|
+
ws = new WebSocket(getUrl());
|
|
102
|
+
ws.onopen = () => {
|
|
103
|
+
backoffMs = BASE_BACKOFF_MS;
|
|
104
|
+
if (hasConnectedBefore) {
|
|
105
|
+
onReconnect();
|
|
106
|
+
}
|
|
107
|
+
hasConnectedBefore = true;
|
|
108
|
+
};
|
|
109
|
+
ws.onmessage = (event) => {
|
|
110
|
+
try {
|
|
111
|
+
const data = JSON.parse(event.data);
|
|
112
|
+
onEvent(data);
|
|
113
|
+
} catch {}
|
|
114
|
+
};
|
|
115
|
+
ws.onclose = () => {
|
|
116
|
+
ws = null;
|
|
117
|
+
if (!intentionalDisconnect && !disposed) {
|
|
118
|
+
scheduleReconnect();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
ws.onerror = () => {};
|
|
122
|
+
}
|
|
123
|
+
function connect() {
|
|
124
|
+
intentionalDisconnect = false;
|
|
125
|
+
doConnect();
|
|
126
|
+
}
|
|
127
|
+
function disconnect() {
|
|
128
|
+
intentionalDisconnect = true;
|
|
129
|
+
clearReconnectTimer();
|
|
130
|
+
if (ws) {
|
|
131
|
+
ws.onclose = null;
|
|
132
|
+
ws.close();
|
|
133
|
+
ws = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function dispose() {
|
|
137
|
+
disposed = true;
|
|
138
|
+
disconnect();
|
|
139
|
+
}
|
|
140
|
+
return { connect, disconnect, dispose };
|
|
141
|
+
}
|
|
142
|
+
// src/auth/access-gate.ts
|
|
143
|
+
function AccessGate({
|
|
144
|
+
fallback,
|
|
145
|
+
children
|
|
146
|
+
}) {
|
|
147
|
+
const ctx = useContext(AccessContext);
|
|
148
|
+
if (!ctx) {
|
|
149
|
+
return typeof children === "function" ? children() : children;
|
|
150
|
+
}
|
|
151
|
+
const isLoaded = computed(() => {
|
|
152
|
+
const set = ctx.accessSet;
|
|
153
|
+
return set !== null;
|
|
154
|
+
});
|
|
155
|
+
return computed(() => {
|
|
156
|
+
if (isLoaded.value) {
|
|
157
|
+
return typeof children === "function" ? children() : children;
|
|
158
|
+
}
|
|
159
|
+
return fallback ? fallback() : null;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// src/auth/access-event-handler.ts
|
|
163
|
+
function handleAccessEvent(accessSet, event, flagEntitlementMap) {
|
|
164
|
+
const current = accessSet.value;
|
|
165
|
+
if (!current)
|
|
166
|
+
return;
|
|
167
|
+
switch (event.type) {
|
|
168
|
+
case "access:flag_toggled":
|
|
169
|
+
handleFlagToggle(accessSet, current, event.flag, event.enabled, flagEntitlementMap);
|
|
170
|
+
break;
|
|
171
|
+
case "access:limit_updated":
|
|
172
|
+
handleLimitUpdate(accessSet, current, event.entitlement, event.consumed, event.remaining, event.max);
|
|
173
|
+
break;
|
|
174
|
+
case "access:role_changed":
|
|
175
|
+
case "access:plan_changed":
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function handleFlagToggle(accessSet, current, flag, enabled, flagEntitlementMap) {
|
|
180
|
+
const newFlags = { ...current.flags, [flag]: enabled };
|
|
181
|
+
const newEntitlements = { ...current.entitlements };
|
|
182
|
+
if (flagEntitlementMap) {
|
|
183
|
+
for (const [name, requiredFlags] of Object.entries(flagEntitlementMap)) {
|
|
184
|
+
if (!requiredFlags.includes(flag))
|
|
185
|
+
continue;
|
|
186
|
+
if (!enabled) {
|
|
187
|
+
newEntitlements[name] = {
|
|
188
|
+
allowed: false,
|
|
189
|
+
reasons: ["flag_disabled"],
|
|
190
|
+
reason: "flag_disabled",
|
|
191
|
+
meta: { disabledFlags: [flag] }
|
|
192
|
+
};
|
|
193
|
+
} else {
|
|
194
|
+
const existing = newEntitlements[name];
|
|
195
|
+
if (existing?.reason === "flag_disabled") {
|
|
196
|
+
newEntitlements[name] = { allowed: true, reasons: [] };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
accessSet.value = { ...current, flags: newFlags, entitlements: newEntitlements };
|
|
202
|
+
}
|
|
203
|
+
function handleLimitUpdate(accessSet, current, entitlement, consumed, remaining, max) {
|
|
204
|
+
const existingEntry = current.entitlements[entitlement];
|
|
205
|
+
if (!existingEntry)
|
|
206
|
+
return;
|
|
207
|
+
const newLimit = { max, consumed, remaining };
|
|
208
|
+
const newEntitlements = { ...current.entitlements };
|
|
209
|
+
if (remaining <= 0) {
|
|
210
|
+
const reasons = [...existingEntry.reasons];
|
|
211
|
+
if (!reasons.includes("limit_reached"))
|
|
212
|
+
reasons.push("limit_reached");
|
|
213
|
+
const entry = {
|
|
214
|
+
...existingEntry,
|
|
215
|
+
allowed: false,
|
|
216
|
+
reasons,
|
|
217
|
+
reason: reasons[0],
|
|
218
|
+
meta: { ...existingEntry.meta, limit: newLimit }
|
|
219
|
+
};
|
|
220
|
+
newEntitlements[entitlement] = entry;
|
|
221
|
+
} else {
|
|
222
|
+
newEntitlements[entitlement] = {
|
|
223
|
+
...existingEntry,
|
|
224
|
+
meta: { ...existingEntry.meta, limit: newLimit }
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
accessSet.value = { ...current, entitlements: newEntitlements };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/auth/auth-client.ts
|
|
231
|
+
import { err, ok } from "@vertz/fetch";
|
|
232
|
+
async function parseAuthError(res) {
|
|
233
|
+
let code = "SERVER_ERROR";
|
|
234
|
+
let message = "An unexpected error occurred";
|
|
235
|
+
let retryAfter;
|
|
236
|
+
try {
|
|
237
|
+
const body = await res.json();
|
|
238
|
+
if (body.code)
|
|
239
|
+
code = body.code;
|
|
240
|
+
if (body.message)
|
|
241
|
+
message = body.message;
|
|
242
|
+
} catch {}
|
|
243
|
+
if (res.status === 401) {
|
|
244
|
+
code = code === "SERVER_ERROR" ? "INVALID_CREDENTIALS" : code;
|
|
245
|
+
message = message === "An unexpected error occurred" ? "Invalid email or password" : message;
|
|
246
|
+
} else if (res.status === 409) {
|
|
247
|
+
code = code === "SERVER_ERROR" ? "USER_EXISTS" : code;
|
|
248
|
+
message = message === "An unexpected error occurred" ? "An account with this email already exists" : message;
|
|
249
|
+
} else if (res.status === 429) {
|
|
250
|
+
code = "RATE_LIMITED";
|
|
251
|
+
const retryHeader = res.headers.get("Retry-After");
|
|
252
|
+
if (retryHeader)
|
|
253
|
+
retryAfter = Number.parseInt(retryHeader, 10);
|
|
254
|
+
}
|
|
255
|
+
return { code, message, statusCode: res.status, retryAfter };
|
|
256
|
+
}
|
|
257
|
+
function createAuthMethod({
|
|
258
|
+
basePath,
|
|
259
|
+
endpoint,
|
|
260
|
+
httpMethod,
|
|
261
|
+
schema,
|
|
262
|
+
onSuccess
|
|
263
|
+
}) {
|
|
264
|
+
const url = `${basePath}/${endpoint}`;
|
|
265
|
+
const fn = async (body) => {
|
|
266
|
+
let res;
|
|
267
|
+
try {
|
|
268
|
+
res = await fetch(url, {
|
|
269
|
+
method: httpMethod,
|
|
270
|
+
headers: {
|
|
271
|
+
"Content-Type": "application/json",
|
|
272
|
+
"X-VTZ-Request": "1"
|
|
273
|
+
},
|
|
274
|
+
credentials: "include",
|
|
275
|
+
body: JSON.stringify(body)
|
|
276
|
+
});
|
|
277
|
+
} catch (e) {
|
|
278
|
+
const networkError = {
|
|
279
|
+
code: "NETWORK_ERROR",
|
|
280
|
+
message: e instanceof Error ? e.message : "Network error",
|
|
281
|
+
statusCode: 0
|
|
282
|
+
};
|
|
283
|
+
return err(Object.assign(new Error(networkError.message), networkError));
|
|
284
|
+
}
|
|
285
|
+
if (!res.ok) {
|
|
286
|
+
const authError = await parseAuthError(res);
|
|
287
|
+
return err(Object.assign(new Error(authError.message), authError));
|
|
288
|
+
}
|
|
289
|
+
const data = await res.json();
|
|
290
|
+
onSuccess(data);
|
|
291
|
+
return ok(data);
|
|
292
|
+
};
|
|
293
|
+
return Object.assign(fn, {
|
|
294
|
+
url,
|
|
295
|
+
method: httpMethod,
|
|
296
|
+
meta: { bodySchema: schema }
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/auth/auth-types.ts
|
|
301
|
+
var signInSchema = {
|
|
302
|
+
parse(data) {
|
|
303
|
+
const d = data;
|
|
304
|
+
const errors = [];
|
|
305
|
+
if (!d.email || typeof d.email !== "string" || !d.email.includes("@")) {
|
|
306
|
+
errors.push({ path: ["email"], message: "Valid email is required" });
|
|
307
|
+
}
|
|
308
|
+
if (!d.password || typeof d.password !== "string") {
|
|
309
|
+
errors.push({ path: ["password"], message: "Password is required" });
|
|
310
|
+
}
|
|
311
|
+
if (errors.length > 0) {
|
|
312
|
+
const err2 = new Error("Validation failed");
|
|
313
|
+
err2.issues = errors;
|
|
314
|
+
return { ok: false, error: err2 };
|
|
315
|
+
}
|
|
316
|
+
return { ok: true, data: { email: d.email, password: d.password } };
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
var signUpSchema = {
|
|
320
|
+
parse(data) {
|
|
321
|
+
const d = data;
|
|
322
|
+
const errors = [];
|
|
323
|
+
if (!d.email || typeof d.email !== "string" || !d.email.includes("@")) {
|
|
324
|
+
errors.push({ path: ["email"], message: "Valid email is required" });
|
|
325
|
+
}
|
|
326
|
+
if (!d.password || typeof d.password !== "string" || d.password.length < 8) {
|
|
327
|
+
errors.push({ path: ["password"], message: "Password must be at least 8 characters" });
|
|
328
|
+
}
|
|
329
|
+
if (errors.length > 0) {
|
|
330
|
+
const err2 = new Error("Validation failed");
|
|
331
|
+
err2.issues = errors;
|
|
332
|
+
return { ok: false, error: err2 };
|
|
333
|
+
}
|
|
334
|
+
const { email, password, ...rest } = d;
|
|
335
|
+
return {
|
|
336
|
+
ok: true,
|
|
337
|
+
data: { email, password, ...rest }
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
var mfaSchema = {
|
|
342
|
+
parse(data) {
|
|
343
|
+
const d = data;
|
|
344
|
+
if (!d.code || typeof d.code !== "string" || d.code.length !== 6) {
|
|
345
|
+
const err2 = new Error("Validation failed");
|
|
346
|
+
err2.issues = [
|
|
347
|
+
{ path: ["code"], message: "Enter a 6-digit code" }
|
|
348
|
+
];
|
|
349
|
+
return { ok: false, error: err2 };
|
|
350
|
+
}
|
|
351
|
+
return { ok: true, data: { code: d.code } };
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
var forgotPasswordSchema = {
|
|
355
|
+
parse(data) {
|
|
356
|
+
const d = data;
|
|
357
|
+
if (!d.email || typeof d.email !== "string" || !d.email.includes("@")) {
|
|
358
|
+
const err2 = new Error("Validation failed");
|
|
359
|
+
err2.issues = [
|
|
360
|
+
{ path: ["email"], message: "Valid email is required" }
|
|
361
|
+
];
|
|
362
|
+
return { ok: false, error: err2 };
|
|
363
|
+
}
|
|
364
|
+
return { ok: true, data: { email: d.email } };
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
var resetPasswordSchema = {
|
|
368
|
+
parse(data) {
|
|
369
|
+
const d = data;
|
|
370
|
+
const errors = [];
|
|
371
|
+
if (!d.token || typeof d.token !== "string") {
|
|
372
|
+
errors.push({ path: ["token"], message: "Token is required" });
|
|
373
|
+
}
|
|
374
|
+
if (!d.password || typeof d.password !== "string" || d.password.length < 8) {
|
|
375
|
+
errors.push({ path: ["password"], message: "Password must be at least 8 characters" });
|
|
376
|
+
}
|
|
377
|
+
if (errors.length > 0) {
|
|
378
|
+
const err2 = new Error("Validation failed");
|
|
379
|
+
err2.issues = errors;
|
|
380
|
+
return { ok: false, error: err2 };
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
ok: true,
|
|
384
|
+
data: { token: d.token, password: d.password }
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/auth/token-refresh.ts
|
|
390
|
+
var REFRESH_MARGIN_MS = 1e4;
|
|
391
|
+
function createTokenRefresh({ onRefresh }) {
|
|
392
|
+
let timerId;
|
|
393
|
+
let inflightPromise = null;
|
|
394
|
+
let lastExpiresAt = null;
|
|
395
|
+
let pendingOfflineRefresh = false;
|
|
396
|
+
function schedule(expiresAt) {
|
|
397
|
+
lastExpiresAt = expiresAt;
|
|
398
|
+
pendingOfflineRefresh = false;
|
|
399
|
+
clearTimer();
|
|
400
|
+
const delay = Math.max(0, expiresAt - Date.now() - REFRESH_MARGIN_MS);
|
|
401
|
+
timerId = setTimeout(() => {
|
|
402
|
+
executeRefresh();
|
|
403
|
+
}, delay);
|
|
404
|
+
}
|
|
405
|
+
function executeRefresh() {
|
|
406
|
+
if (inflightPromise)
|
|
407
|
+
return;
|
|
408
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) {
|
|
409
|
+
pendingOfflineRefresh = true;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
inflightPromise = onRefresh().finally(() => {
|
|
413
|
+
inflightPromise = null;
|
|
414
|
+
});
|
|
415
|
+
inflightPromise.catch(() => {});
|
|
416
|
+
}
|
|
417
|
+
function clearTimer() {
|
|
418
|
+
if (timerId !== undefined) {
|
|
419
|
+
clearTimeout(timerId);
|
|
420
|
+
timerId = undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function cancel() {
|
|
424
|
+
clearTimer();
|
|
425
|
+
lastExpiresAt = null;
|
|
426
|
+
pendingOfflineRefresh = false;
|
|
427
|
+
}
|
|
428
|
+
let visibilityHandler;
|
|
429
|
+
if (typeof document !== "undefined") {
|
|
430
|
+
visibilityHandler = () => {
|
|
431
|
+
if (document.visibilityState === "hidden") {
|
|
432
|
+
clearTimer();
|
|
433
|
+
} else if (lastExpiresAt !== null) {
|
|
434
|
+
schedule(lastExpiresAt);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
document.addEventListener("visibilitychange", visibilityHandler);
|
|
438
|
+
}
|
|
439
|
+
let onlineHandler;
|
|
440
|
+
if (typeof window !== "undefined") {
|
|
441
|
+
onlineHandler = () => {
|
|
442
|
+
if (pendingOfflineRefresh) {
|
|
443
|
+
pendingOfflineRefresh = false;
|
|
444
|
+
executeRefresh();
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
window.addEventListener("online", onlineHandler);
|
|
448
|
+
}
|
|
449
|
+
function dispose() {
|
|
450
|
+
cancel();
|
|
451
|
+
if (visibilityHandler && typeof document !== "undefined") {
|
|
452
|
+
document.removeEventListener("visibilitychange", visibilityHandler);
|
|
453
|
+
}
|
|
454
|
+
if (onlineHandler && typeof window !== "undefined") {
|
|
455
|
+
window.removeEventListener("online", onlineHandler);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return { schedule, cancel, dispose };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/auth/auth-context.ts
|
|
462
|
+
var AuthContext = createContext(undefined, "@vertz/ui::AuthContext");
|
|
463
|
+
function useAuth() {
|
|
464
|
+
const ctx = useContext(AuthContext);
|
|
465
|
+
if (!ctx)
|
|
466
|
+
throw new Error("useAuth must be called within AuthProvider");
|
|
467
|
+
return ctx;
|
|
468
|
+
}
|
|
469
|
+
function AuthProvider({
|
|
470
|
+
basePath = "/api/auth",
|
|
471
|
+
accessControl,
|
|
472
|
+
accessEvents,
|
|
473
|
+
accessEventsUrl,
|
|
474
|
+
flagEntitlementMap,
|
|
475
|
+
children
|
|
476
|
+
}) {
|
|
477
|
+
const userSignal = signal(null);
|
|
478
|
+
const statusSignal = signal("idle");
|
|
479
|
+
const errorSignal = signal(null);
|
|
480
|
+
const isAuthenticated = computed(() => statusSignal.value === "authenticated");
|
|
481
|
+
const isLoading = computed(() => statusSignal.value === "loading");
|
|
482
|
+
const accessSetSignal = accessControl ? signal(null) : null;
|
|
483
|
+
const accessLoadingSignal = accessControl ? signal(true) : null;
|
|
484
|
+
const tokenRefresh = createTokenRefresh({
|
|
485
|
+
onRefresh: async () => {
|
|
486
|
+
await refresh();
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
async function fetchAccessSet() {
|
|
490
|
+
if (!accessSetSignal || !accessLoadingSignal)
|
|
491
|
+
return;
|
|
492
|
+
try {
|
|
493
|
+
const res = await fetch(`${basePath}/access-set`, {
|
|
494
|
+
headers: { "X-VTZ-Request": "1" },
|
|
495
|
+
credentials: "include"
|
|
496
|
+
});
|
|
497
|
+
if (res.ok) {
|
|
498
|
+
accessSetSignal.value = await res.json();
|
|
499
|
+
accessLoadingSignal.value = false;
|
|
500
|
+
}
|
|
501
|
+
} catch {}
|
|
502
|
+
}
|
|
503
|
+
function clearAccessSet() {
|
|
504
|
+
if (!accessSetSignal || !accessLoadingSignal)
|
|
505
|
+
return;
|
|
506
|
+
accessSetSignal.value = null;
|
|
507
|
+
accessLoadingSignal.value = true;
|
|
508
|
+
}
|
|
509
|
+
function handleAuthSuccess(data) {
|
|
510
|
+
userSignal.value = data.user;
|
|
511
|
+
statusSignal.value = "authenticated";
|
|
512
|
+
errorSignal.value = null;
|
|
513
|
+
if (data.expiresAt) {
|
|
514
|
+
tokenRefresh.schedule(data.expiresAt);
|
|
515
|
+
}
|
|
516
|
+
fetchAccessSet();
|
|
517
|
+
}
|
|
518
|
+
function handleAuthError(error) {
|
|
519
|
+
if (error.code === "MFA_REQUIRED") {
|
|
520
|
+
statusSignal.value = "mfa_required";
|
|
521
|
+
errorSignal.value = null;
|
|
522
|
+
} else {
|
|
523
|
+
statusSignal.value = "error";
|
|
524
|
+
errorSignal.value = {
|
|
525
|
+
code: error.code ?? "SERVER_ERROR",
|
|
526
|
+
message: error.message,
|
|
527
|
+
statusCode: error.statusCode ?? 0,
|
|
528
|
+
retryAfter: error.retryAfter
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const signInMethod = createAuthMethod({
|
|
533
|
+
basePath,
|
|
534
|
+
endpoint: "signin",
|
|
535
|
+
httpMethod: "POST",
|
|
536
|
+
schema: signInSchema,
|
|
537
|
+
onSuccess: handleAuthSuccess
|
|
538
|
+
});
|
|
539
|
+
const signIn = Object.assign(async (body) => {
|
|
540
|
+
statusSignal.value = "loading";
|
|
541
|
+
errorSignal.value = null;
|
|
542
|
+
const result = await signInMethod(body);
|
|
543
|
+
if (!result.ok) {
|
|
544
|
+
handleAuthError(result.error);
|
|
545
|
+
}
|
|
546
|
+
return result;
|
|
547
|
+
}, {
|
|
548
|
+
url: signInMethod.url,
|
|
549
|
+
method: signInMethod.method,
|
|
550
|
+
meta: signInMethod.meta
|
|
551
|
+
});
|
|
552
|
+
const signUpMethod = createAuthMethod({
|
|
553
|
+
basePath,
|
|
554
|
+
endpoint: "signup",
|
|
555
|
+
httpMethod: "POST",
|
|
556
|
+
schema: signUpSchema,
|
|
557
|
+
onSuccess: handleAuthSuccess
|
|
558
|
+
});
|
|
559
|
+
const signUp = Object.assign(async (body) => {
|
|
560
|
+
statusSignal.value = "loading";
|
|
561
|
+
errorSignal.value = null;
|
|
562
|
+
const result = await signUpMethod(body);
|
|
563
|
+
if (!result.ok) {
|
|
564
|
+
handleAuthError(result.error);
|
|
565
|
+
}
|
|
566
|
+
return result;
|
|
567
|
+
}, {
|
|
568
|
+
url: signUpMethod.url,
|
|
569
|
+
method: signUpMethod.method,
|
|
570
|
+
meta: signUpMethod.meta
|
|
571
|
+
});
|
|
572
|
+
const mfaChallengeMethod = createAuthMethod({
|
|
573
|
+
basePath,
|
|
574
|
+
endpoint: "mfa/challenge",
|
|
575
|
+
httpMethod: "POST",
|
|
576
|
+
schema: mfaSchema,
|
|
577
|
+
onSuccess: handleAuthSuccess
|
|
578
|
+
});
|
|
579
|
+
const mfaChallenge = Object.assign(async (body) => {
|
|
580
|
+
statusSignal.value = "loading";
|
|
581
|
+
errorSignal.value = null;
|
|
582
|
+
const result = await mfaChallengeMethod(body);
|
|
583
|
+
if (!result.ok) {
|
|
584
|
+
handleAuthError(result.error);
|
|
585
|
+
}
|
|
586
|
+
return result;
|
|
587
|
+
}, {
|
|
588
|
+
url: mfaChallengeMethod.url,
|
|
589
|
+
method: mfaChallengeMethod.method,
|
|
590
|
+
meta: mfaChallengeMethod.meta
|
|
591
|
+
});
|
|
592
|
+
const forgotPasswordMethod = createAuthMethod({
|
|
593
|
+
basePath,
|
|
594
|
+
endpoint: "forgot-password",
|
|
595
|
+
httpMethod: "POST",
|
|
596
|
+
schema: forgotPasswordSchema,
|
|
597
|
+
onSuccess: () => {}
|
|
598
|
+
});
|
|
599
|
+
const forgotPassword = Object.assign(async (body) => {
|
|
600
|
+
return forgotPasswordMethod(body);
|
|
601
|
+
}, {
|
|
602
|
+
url: forgotPasswordMethod.url,
|
|
603
|
+
method: forgotPasswordMethod.method,
|
|
604
|
+
meta: forgotPasswordMethod.meta
|
|
605
|
+
});
|
|
606
|
+
const resetPasswordMethod = createAuthMethod({
|
|
607
|
+
basePath,
|
|
608
|
+
endpoint: "reset-password",
|
|
609
|
+
httpMethod: "POST",
|
|
610
|
+
schema: resetPasswordSchema,
|
|
611
|
+
onSuccess: () => {}
|
|
612
|
+
});
|
|
613
|
+
const resetPassword = Object.assign(async (body) => {
|
|
614
|
+
return resetPasswordMethod(body);
|
|
615
|
+
}, {
|
|
616
|
+
url: resetPasswordMethod.url,
|
|
617
|
+
method: resetPasswordMethod.method,
|
|
618
|
+
meta: resetPasswordMethod.meta
|
|
619
|
+
});
|
|
620
|
+
const signOut = async () => {
|
|
621
|
+
tokenRefresh.cancel();
|
|
622
|
+
try {
|
|
623
|
+
await fetch(`${basePath}/signout`, {
|
|
624
|
+
method: "POST",
|
|
625
|
+
headers: { "X-VTZ-Request": "1" },
|
|
626
|
+
credentials: "include"
|
|
627
|
+
});
|
|
628
|
+
} catch {}
|
|
629
|
+
userSignal.value = null;
|
|
630
|
+
statusSignal.value = "unauthenticated";
|
|
631
|
+
errorSignal.value = null;
|
|
632
|
+
clearAccessSet();
|
|
633
|
+
if (typeof window !== "undefined") {
|
|
634
|
+
delete window.__VERTZ_SESSION__;
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
let refreshInFlight = null;
|
|
638
|
+
const doRefresh = async () => {
|
|
639
|
+
statusSignal.value = "loading";
|
|
640
|
+
try {
|
|
641
|
+
const res = await fetch(`${basePath}/refresh`, {
|
|
642
|
+
method: "POST",
|
|
643
|
+
headers: { "X-VTZ-Request": "1" },
|
|
644
|
+
credentials: "include"
|
|
645
|
+
});
|
|
646
|
+
if (res.ok) {
|
|
647
|
+
const data = await res.json();
|
|
648
|
+
handleAuthSuccess(data);
|
|
649
|
+
} else {
|
|
650
|
+
userSignal.value = null;
|
|
651
|
+
statusSignal.value = "unauthenticated";
|
|
652
|
+
errorSignal.value = null;
|
|
653
|
+
}
|
|
654
|
+
} catch {
|
|
655
|
+
userSignal.value = null;
|
|
656
|
+
statusSignal.value = "unauthenticated";
|
|
657
|
+
errorSignal.value = null;
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
const refresh = async () => {
|
|
661
|
+
if (refreshInFlight)
|
|
662
|
+
return refreshInFlight;
|
|
663
|
+
refreshInFlight = doRefresh().finally(() => {
|
|
664
|
+
refreshInFlight = null;
|
|
665
|
+
});
|
|
666
|
+
return refreshInFlight;
|
|
667
|
+
};
|
|
668
|
+
const eventClient = accessControl && accessEvents && accessSetSignal ? createAccessEventClient({
|
|
669
|
+
url: accessEventsUrl,
|
|
670
|
+
onEvent: (event) => {
|
|
671
|
+
if (!accessSetSignal)
|
|
672
|
+
return;
|
|
673
|
+
if (event.type === "access:flag_toggled" || event.type === "access:limit_updated") {
|
|
674
|
+
handleAccessEvent(accessSetSignal, event, flagEntitlementMap);
|
|
675
|
+
} else {
|
|
676
|
+
const jitter = Math.random() * 1000;
|
|
677
|
+
setTimeout(() => {
|
|
678
|
+
fetchAccessSet();
|
|
679
|
+
}, jitter);
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
onReconnect: () => {
|
|
683
|
+
fetchAccessSet();
|
|
684
|
+
}
|
|
685
|
+
}) : null;
|
|
686
|
+
if (eventClient) {
|
|
687
|
+
eventClient.connect();
|
|
688
|
+
}
|
|
689
|
+
_tryOnCleanup(() => {
|
|
690
|
+
tokenRefresh.dispose();
|
|
691
|
+
eventClient?.dispose();
|
|
692
|
+
});
|
|
693
|
+
const contextValue = {
|
|
694
|
+
user: userSignal,
|
|
695
|
+
status: statusSignal,
|
|
696
|
+
isAuthenticated,
|
|
697
|
+
isLoading,
|
|
698
|
+
error: errorSignal,
|
|
699
|
+
signIn,
|
|
700
|
+
signUp,
|
|
701
|
+
signOut,
|
|
702
|
+
refresh,
|
|
703
|
+
mfaChallenge,
|
|
704
|
+
forgotPassword,
|
|
705
|
+
resetPassword
|
|
706
|
+
};
|
|
707
|
+
if (typeof window !== "undefined") {
|
|
708
|
+
if (window.__VERTZ_SESSION__?.user) {
|
|
709
|
+
const session = window.__VERTZ_SESSION__;
|
|
710
|
+
userSignal.value = session.user;
|
|
711
|
+
statusSignal.value = "authenticated";
|
|
712
|
+
if (session.expiresAt) {
|
|
713
|
+
tokenRefresh.schedule(session.expiresAt);
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
statusSignal.value = "unauthenticated";
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (accessControl && accessSetSignal && accessLoadingSignal) {
|
|
720
|
+
if (typeof window !== "undefined" && window.__VERTZ_ACCESS_SET__ && typeof window.__VERTZ_ACCESS_SET__.entitlements === "object" && window.__VERTZ_ACCESS_SET__.entitlements !== null) {
|
|
721
|
+
accessSetSignal.value = window.__VERTZ_ACCESS_SET__;
|
|
722
|
+
accessLoadingSignal.value = false;
|
|
723
|
+
}
|
|
724
|
+
const accessValue = { accessSet: accessSetSignal, loading: accessLoadingSignal };
|
|
725
|
+
return AuthContext.Provider({
|
|
726
|
+
value: contextValue,
|
|
727
|
+
children: () => AccessContext.Provider({
|
|
728
|
+
value: accessValue,
|
|
729
|
+
children
|
|
730
|
+
})
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
return AuthContext.Provider({ value: contextValue, children });
|
|
734
|
+
}
|
|
735
|
+
// src/auth/auth-gate.ts
|
|
736
|
+
function AuthGate({ fallback, children }) {
|
|
737
|
+
const ctx = useContext(AuthContext);
|
|
738
|
+
if (!ctx) {
|
|
739
|
+
return typeof children === "function" ? children() : children;
|
|
740
|
+
}
|
|
741
|
+
const isResolved = computed(() => {
|
|
742
|
+
const status = ctx.status;
|
|
743
|
+
return status !== "idle" && status !== "loading";
|
|
744
|
+
});
|
|
745
|
+
return computed(() => {
|
|
746
|
+
if (isResolved.value) {
|
|
747
|
+
return typeof children === "function" ? children() : children;
|
|
748
|
+
}
|
|
749
|
+
return fallback ? fallback() : null;
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
// src/auth/create-access-provider.ts
|
|
753
|
+
function createAccessProvider() {
|
|
754
|
+
const accessSet = signal(null);
|
|
755
|
+
const loading = signal(true);
|
|
756
|
+
if (typeof window !== "undefined" && window.__VERTZ_ACCESS_SET__ && typeof window.__VERTZ_ACCESS_SET__.entitlements === "object" && window.__VERTZ_ACCESS_SET__.entitlements !== null) {
|
|
757
|
+
accessSet.value = window.__VERTZ_ACCESS_SET__;
|
|
758
|
+
loading.value = false;
|
|
759
|
+
}
|
|
760
|
+
return { accessSet, loading };
|
|
761
|
+
}
|
|
762
|
+
export {
|
|
763
|
+
useAuth,
|
|
764
|
+
useAccessContext,
|
|
765
|
+
createAccessProvider,
|
|
766
|
+
createAccessEventClient,
|
|
767
|
+
can,
|
|
768
|
+
AuthProvider,
|
|
769
|
+
AuthGate,
|
|
770
|
+
AuthContext,
|
|
771
|
+
AccessGate,
|
|
772
|
+
AccessContext
|
|
773
|
+
};
|