@vertz/ui 0.2.14 → 0.2.16
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 +49 -0
- package/dist/shared/chunk-14eqne2a.js +10 -0
- package/dist/shared/{chunk-dksg08fq.js → chunk-1rxa2fz4.js} +2 -8
- package/dist/shared/{chunk-8hsz5y4a.js → chunk-4fwcwxn6.js} +14 -4
- package/dist/shared/{chunk-hw67ckr3.js → chunk-4mtn7af6.js} +230 -19
- package/dist/shared/{chunk-2sth83bd.js → chunk-6jyt4ycw.js} +67 -2
- package/dist/shared/{chunk-83g4h38e.js → chunk-6wd36w21.js} +1 -0
- package/dist/shared/{chunk-h89w580h.js → chunk-afawz764.js} +1 -1
- package/dist/shared/{chunk-nn9v1zmk.js → chunk-b0qqqk03.js} +86 -21
- package/dist/shared/{chunk-c9xxsrat.js → chunk-dhehvmj0.js} +179 -10
- package/dist/shared/{chunk-mj7b4t40.js → chunk-fkbgbf3n.js} +98 -11
- package/dist/shared/{chunk-c30eg6wn.js → chunk-j09yyh34.js} +79 -6
- package/dist/shared/chunk-mtsvrj9e.js +23 -0
- package/dist/shared/chunk-pnv25zep.js +7 -0
- package/dist/shared/{chunk-j6qyxfdc.js → chunk-vndfjfdy.js} +3 -3
- package/dist/src/auth/public.d.ts +69 -9
- package/dist/src/auth/public.js +217 -13
- package/dist/src/css/public.d.ts +110 -2
- package/dist/src/css/public.js +8 -4
- package/dist/src/form/public.d.ts +33 -7
- package/dist/src/form/public.js +2 -2
- package/dist/src/index.d.ts +311 -20
- package/dist/src/index.js +161 -14
- package/dist/src/internals.d.ts +141 -5
- package/dist/src/internals.js +17 -9
- package/dist/src/query/public.js +4 -3
- package/dist/src/router/public.d.ts +39 -9
- package/dist/src/router/public.js +17 -11
- package/dist/src/test/index.d.ts +26 -23
- package/dist/src/test/index.js +5 -4
- package/package.json +3 -3
- package/reactivity.json +1 -11
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
computed,
|
|
3
3
|
signal
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-4fwcwxn6.js";
|
|
5
5
|
|
|
6
6
|
// src/form/field-state.ts
|
|
7
7
|
function createFieldState(_name, initialValue) {
|
|
@@ -31,14 +31,40 @@ function createFieldState(_name, initialValue) {
|
|
|
31
31
|
function formDataToObject(formData, options) {
|
|
32
32
|
const result = {};
|
|
33
33
|
const coerce = options?.coerce ?? false;
|
|
34
|
+
const nested = options?.nested ?? false;
|
|
34
35
|
for (const [key, value] of formData.entries()) {
|
|
35
36
|
if (typeof value !== "string") {
|
|
36
37
|
continue;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
const coerced = coerce ? coerceValue(value) : value;
|
|
40
|
+
if (nested && key.includes(".")) {
|
|
41
|
+
setNestedValue(result, key, coerced);
|
|
42
|
+
} else {
|
|
43
|
+
result[key] = coerced;
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
return result;
|
|
41
47
|
}
|
|
48
|
+
var DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
49
|
+
function setNestedValue(obj, dotPath, value) {
|
|
50
|
+
const segments = dotPath.split(".");
|
|
51
|
+
let current = obj;
|
|
52
|
+
for (let i = 0;i < segments.length - 1; i++) {
|
|
53
|
+
const segment = segments[i];
|
|
54
|
+
if (DANGEROUS_KEYS.has(segment))
|
|
55
|
+
return;
|
|
56
|
+
const nextSegment = segments[i + 1];
|
|
57
|
+
const isNextArray = /^\d+$/.test(nextSegment);
|
|
58
|
+
if (!(segment in current)) {
|
|
59
|
+
current[segment] = isNextArray ? [] : {};
|
|
60
|
+
}
|
|
61
|
+
current = current[segment];
|
|
62
|
+
}
|
|
63
|
+
const lastSegment = segments[segments.length - 1];
|
|
64
|
+
if (DANGEROUS_KEYS.has(lastSegment))
|
|
65
|
+
return;
|
|
66
|
+
current[lastSegment] = value;
|
|
67
|
+
}
|
|
42
68
|
function coerceValue(value) {
|
|
43
69
|
if (value === "true")
|
|
44
70
|
return true;
|
|
@@ -79,8 +105,11 @@ function validate(schema, data) {
|
|
|
79
105
|
}
|
|
80
106
|
|
|
81
107
|
// src/form/form.ts
|
|
108
|
+
var FIELD_STATE_SIGNALS = new Set(["error", "dirty", "touched", "value"]);
|
|
109
|
+
var FIELD_STATE_METHODS = new Set(["setValue", "reset"]);
|
|
82
110
|
function form(sdkMethod, options) {
|
|
83
111
|
const fieldCache = new Map;
|
|
112
|
+
const chainProxyCache = new Map;
|
|
84
113
|
const submitting = signal(false);
|
|
85
114
|
const fieldGeneration = signal(0);
|
|
86
115
|
const dirty = computed(() => {
|
|
@@ -99,20 +128,57 @@ function form(sdkMethod, options) {
|
|
|
99
128
|
}
|
|
100
129
|
return true;
|
|
101
130
|
});
|
|
131
|
+
function resolveNestedInitial(dotPath) {
|
|
132
|
+
const initialObj = typeof options?.initial === "function" ? options.initial() : options?.initial;
|
|
133
|
+
if (!initialObj)
|
|
134
|
+
return;
|
|
135
|
+
const segments = dotPath.split(".");
|
|
136
|
+
let current = initialObj;
|
|
137
|
+
for (const segment of segments) {
|
|
138
|
+
if (current == null || typeof current !== "object")
|
|
139
|
+
return;
|
|
140
|
+
current = current[segment];
|
|
141
|
+
}
|
|
142
|
+
return current;
|
|
143
|
+
}
|
|
102
144
|
function getOrCreateField(name) {
|
|
103
145
|
let field = fieldCache.get(name);
|
|
104
146
|
if (!field) {
|
|
105
|
-
const
|
|
106
|
-
|
|
147
|
+
const initialValue = name.includes(".") ? resolveNestedInitial(name) : (() => {
|
|
148
|
+
const initialObj = typeof options?.initial === "function" ? options.initial() : options?.initial;
|
|
149
|
+
return initialObj?.[name];
|
|
150
|
+
})();
|
|
107
151
|
field = createFieldState(name, initialValue);
|
|
108
152
|
fieldCache.set(name, field);
|
|
109
153
|
fieldGeneration.value++;
|
|
110
154
|
}
|
|
111
155
|
return field;
|
|
112
156
|
}
|
|
157
|
+
function getOrCreateChainProxy(dotPath) {
|
|
158
|
+
let proxy = chainProxyCache.get(dotPath);
|
|
159
|
+
if (proxy)
|
|
160
|
+
return proxy;
|
|
161
|
+
proxy = new Proxy(Object.create(null), {
|
|
162
|
+
get(_target, prop) {
|
|
163
|
+
if (typeof prop === "string") {
|
|
164
|
+
if (FIELD_STATE_SIGNALS.has(prop)) {
|
|
165
|
+
return getOrCreateField(dotPath)[prop];
|
|
166
|
+
}
|
|
167
|
+
if (FIELD_STATE_METHODS.has(prop)) {
|
|
168
|
+
return getOrCreateField(dotPath)[prop];
|
|
169
|
+
}
|
|
170
|
+
const childPath = `${dotPath}.${prop}`;
|
|
171
|
+
return getOrCreateChainProxy(childPath);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
chainProxyCache.set(dotPath, proxy);
|
|
177
|
+
return proxy;
|
|
178
|
+
}
|
|
113
179
|
const resolvedSchema = options?.schema ?? sdkMethod.meta?.bodySchema;
|
|
114
180
|
async function submitPipeline(formData) {
|
|
115
|
-
const data = formDataToObject(formData);
|
|
181
|
+
const data = formDataToObject(formData, { nested: true });
|
|
116
182
|
if (resolvedSchema) {
|
|
117
183
|
const result2 = validate(resolvedSchema, data);
|
|
118
184
|
if (!result2.success) {
|
|
@@ -206,6 +272,13 @@ function form(sdkMethod, options) {
|
|
|
206
272
|
submitting,
|
|
207
273
|
dirty,
|
|
208
274
|
valid,
|
|
275
|
+
fields: new Proxy(Object.create(null), {
|
|
276
|
+
get(_target, prop) {
|
|
277
|
+
if (typeof prop === "string")
|
|
278
|
+
return prop;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}),
|
|
209
282
|
__bindElement: (el) => {
|
|
210
283
|
boundElement = el;
|
|
211
284
|
el.addEventListener("input", handleInputOrChange);
|
|
@@ -220,7 +293,7 @@ function form(sdkMethod, options) {
|
|
|
220
293
|
if (knownProperties.has(prop)) {
|
|
221
294
|
return target[prop];
|
|
222
295
|
}
|
|
223
|
-
return
|
|
296
|
+
return getOrCreateChainProxy(prop);
|
|
224
297
|
}
|
|
225
298
|
return Reflect.get(target, prop, receiver);
|
|
226
299
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext
|
|
4
|
+
} from "./chunk-4fwcwxn6.js";
|
|
5
|
+
|
|
6
|
+
// src/router/router-context.ts
|
|
7
|
+
var RouterContext = createContext(undefined, "@vertz/ui::RouterContext");
|
|
8
|
+
function useRouter() {
|
|
9
|
+
const router = useContext(RouterContext);
|
|
10
|
+
if (!router) {
|
|
11
|
+
throw new Error("useRouter() must be called within RouterContext.Provider");
|
|
12
|
+
}
|
|
13
|
+
return router;
|
|
14
|
+
}
|
|
15
|
+
function useParams() {
|
|
16
|
+
const router = useContext(RouterContext);
|
|
17
|
+
if (!router) {
|
|
18
|
+
throw new Error("useParams() must be called within RouterContext.Provider");
|
|
19
|
+
}
|
|
20
|
+
return router.current?.parsedParams ?? router.current?.params ?? {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { RouterContext, useRouter, useParams };
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
getAdapter,
|
|
8
8
|
isRenderNode
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-afawz764.js";
|
|
10
10
|
import {
|
|
11
11
|
domEffect
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-4fwcwxn6.js";
|
|
13
13
|
|
|
14
14
|
// src/hydrate/hydration-context.ts
|
|
15
15
|
var isHydrating = false;
|
|
@@ -371,4 +371,4 @@ function __exitChildren() {
|
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
export { startHydration, endHydration, getIsHydrating, claimText, claimComment, __text, __child, __insert, __element, __append, __staticText, __enterChildren, __exitChildren };
|
|
374
|
+
export { startHydration, endHydration, getIsHydrating, claimElement, claimText, claimComment, enterChildren, exitChildren, __text, __child, __insert, __element, __append, __staticText, __enterChildren, __exitChildren };
|
|
@@ -114,6 +114,13 @@ declare const AccessContext: Context<AccessContextValue>;
|
|
|
114
114
|
*/
|
|
115
115
|
declare function useAccessContext(): UnwrapSignals<AccessContextValue>;
|
|
116
116
|
/**
|
|
117
|
+
* Entitlement registry — augmented by @vertz/codegen to narrow entitlement strings.
|
|
118
|
+
* When empty (no codegen), Entitlement falls back to `string`.
|
|
119
|
+
*/
|
|
120
|
+
interface EntitlementRegistry {}
|
|
121
|
+
/** Entitlement type — narrows to literal union when codegen populates EntitlementRegistry. */
|
|
122
|
+
type Entitlement = keyof EntitlementRegistry extends never ? string : Extract<keyof EntitlementRegistry, string>;
|
|
123
|
+
/**
|
|
117
124
|
* Check if the current user has a specific entitlement.
|
|
118
125
|
*
|
|
119
126
|
* Must be called in the component body (like query()/form()).
|
|
@@ -126,15 +133,9 @@ declare function useAccessContext(): UnwrapSignals<AccessContextValue>;
|
|
|
126
133
|
* @param entitlement - The entitlement to check
|
|
127
134
|
* @param entity - Optional entity with pre-computed `__access` metadata
|
|
128
135
|
*/
|
|
129
|
-
declare function can(entitlement:
|
|
136
|
+
declare function can(entitlement: Entitlement, entity?: {
|
|
130
137
|
__access?: Record<string, AccessCheckData>;
|
|
131
138
|
}): AccessCheck;
|
|
132
|
-
/**
|
|
133
|
-
* Access Event Client — WebSocket client for real-time access invalidation.
|
|
134
|
-
*
|
|
135
|
-
* Connects to the access event WebSocket endpoint, handles reconnection
|
|
136
|
-
* with exponential backoff, and delivers parsed events to the caller.
|
|
137
|
-
*/
|
|
138
139
|
/** Client-side access events (no orgId/userId — server scopes the broadcast) */
|
|
139
140
|
type ClientAccessEvent = {
|
|
140
141
|
type: "access:flag_toggled";
|
|
@@ -150,6 +151,19 @@ type ClientAccessEvent = {
|
|
|
150
151
|
type: "access:role_changed";
|
|
151
152
|
} | {
|
|
152
153
|
type: "access:plan_changed";
|
|
154
|
+
} | {
|
|
155
|
+
type: "access:plan_assigned";
|
|
156
|
+
planId: string;
|
|
157
|
+
} | {
|
|
158
|
+
type: "access:addon_attached";
|
|
159
|
+
addonId: string;
|
|
160
|
+
} | {
|
|
161
|
+
type: "access:addon_detached";
|
|
162
|
+
addonId: string;
|
|
163
|
+
} | {
|
|
164
|
+
type: "access:limit_reset";
|
|
165
|
+
entitlement: string;
|
|
166
|
+
max: number;
|
|
153
167
|
};
|
|
154
168
|
interface AccessEventClientOptions {
|
|
155
169
|
/** WebSocket URL. Defaults to deriving from window.location. */
|
|
@@ -250,6 +264,15 @@ interface ResetInput {
|
|
|
250
264
|
token: string;
|
|
251
265
|
password: string;
|
|
252
266
|
}
|
|
267
|
+
interface SignOutOptions {
|
|
268
|
+
/** Path to navigate to after sign-out completes. Uses SPA navigation (replace). */
|
|
269
|
+
redirectTo?: string;
|
|
270
|
+
}
|
|
271
|
+
interface OAuthProviderInfo {
|
|
272
|
+
id: string;
|
|
273
|
+
name: string;
|
|
274
|
+
authUrl: string;
|
|
275
|
+
}
|
|
253
276
|
interface AuthResponse {
|
|
254
277
|
user: User;
|
|
255
278
|
expiresAt: number;
|
|
@@ -262,11 +285,12 @@ interface AuthContextValue {
|
|
|
262
285
|
error: Signal<AuthClientError | null>;
|
|
263
286
|
signIn: SdkMethodWithMeta<SignInInput, AuthResponse>;
|
|
264
287
|
signUp: SdkMethodWithMeta<SignUpInput, AuthResponse>;
|
|
265
|
-
signOut: () => Promise<void>;
|
|
288
|
+
signOut: (options?: SignOutOptions) => Promise<void>;
|
|
266
289
|
refresh: () => Promise<void>;
|
|
267
290
|
mfaChallenge: SdkMethodWithMeta<MfaInput, AuthResponse>;
|
|
268
291
|
forgotPassword: SdkMethodWithMeta<ForgotInput, void>;
|
|
269
292
|
resetPassword: SdkMethodWithMeta<ResetInput, void>;
|
|
293
|
+
providers: Signal<OAuthProviderInfo[]>;
|
|
270
294
|
}
|
|
271
295
|
declare const AuthContext: Context<AuthContextValue>;
|
|
272
296
|
declare function useAuth(): UnwrapSignals<AuthContextValue>;
|
|
@@ -300,4 +324,40 @@ declare function AuthGate({ fallback, children }: AuthGateProps): ReadonlySignal
|
|
|
300
324
|
* ```
|
|
301
325
|
*/
|
|
302
326
|
declare function createAccessProvider(): AccessContextValue;
|
|
303
|
-
|
|
327
|
+
interface OAuthButtonProps {
|
|
328
|
+
/** Provider ID (e.g., 'github', 'google') */
|
|
329
|
+
provider: string;
|
|
330
|
+
/** Custom label text. Defaults to "Continue with {Name}". */
|
|
331
|
+
label?: string;
|
|
332
|
+
/** Render icon only, no text. */
|
|
333
|
+
iconOnly?: boolean;
|
|
334
|
+
/** @internal — injected providers array for testing. Uses useAuth() in production. */
|
|
335
|
+
_providers?: OAuthProviderInfo[];
|
|
336
|
+
}
|
|
337
|
+
declare function OAuthButton({ provider, label, iconOnly, _providers }: OAuthButtonProps): Element;
|
|
338
|
+
interface OAuthButtonsProps {
|
|
339
|
+
/** @internal — injected providers array for testing. Uses useAuth() in production. */
|
|
340
|
+
_providers?: OAuthProviderInfo[];
|
|
341
|
+
}
|
|
342
|
+
declare function OAuthButtons({ _providers }?: OAuthButtonsProps): HTMLDivElement;
|
|
343
|
+
interface ProtectedRouteProps {
|
|
344
|
+
/** Path to redirect to when unauthenticated. Default: '/login' */
|
|
345
|
+
loginPath?: string;
|
|
346
|
+
/** Rendered while auth is resolving (idle/loading). Default: null */
|
|
347
|
+
fallback?: () => unknown;
|
|
348
|
+
/** Rendered when authenticated */
|
|
349
|
+
children: (() => unknown) | unknown;
|
|
350
|
+
/** Optional: required entitlements (integrates with can()) */
|
|
351
|
+
requires?: Entitlement[];
|
|
352
|
+
/** Rendered when authenticated but lacking required entitlements. Default: null */
|
|
353
|
+
forbidden?: () => unknown;
|
|
354
|
+
/** Append ?returnTo=<currentPath> when redirecting. Default: true */
|
|
355
|
+
returnTo?: boolean;
|
|
356
|
+
}
|
|
357
|
+
declare function ProtectedRoute({ loginPath, fallback, children, requires, forbidden, returnTo }: ProtectedRouteProps): ReadonlySignal<unknown> | unknown;
|
|
358
|
+
/**
|
|
359
|
+
* Get an SVG icon string for a provider.
|
|
360
|
+
* Returns the built-in icon for known providers, or a generic fallback for unknown.
|
|
361
|
+
*/
|
|
362
|
+
declare function getProviderIcon(providerId: string, size: number): string;
|
|
363
|
+
export { useAuth, useAccessContext, getProviderIcon, createAccessProvider, createAccessEventClient, can, User, SignUpInput, SignOutOptions, SignInInput, ResetInput, ProtectedRouteProps, ProtectedRoute, OAuthProviderInfo, OAuthButtonsProps, OAuthButtons, OAuthButtonProps, OAuthButton, MfaInput, ForgotInput, EntitlementRegistry, Entitlement, DenialReason, DenialMeta, ClientAccessEvent, AuthStatus, AuthResponse, AuthProviderProps, AuthProvider, AuthGateProps, AuthGate, AuthErrorCode, AuthContextValue, AuthContext, AuthClientError, AccessSet, AccessGateProps, AccessGate, AccessEventClientOptions, AccessEventClient, AccessContextValue, AccessContext, AccessCheckData, AccessCheck };
|
package/dist/src/auth/public.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RouterContext
|
|
3
|
+
} from "../../shared/chunk-mtsvrj9e.js";
|
|
4
|
+
import {
|
|
5
|
+
__on
|
|
6
|
+
} from "../../shared/chunk-pnv25zep.js";
|
|
7
|
+
import {
|
|
8
|
+
isBrowser
|
|
9
|
+
} from "../../shared/chunk-14eqne2a.js";
|
|
10
|
+
import {
|
|
11
|
+
__append,
|
|
12
|
+
__element,
|
|
13
|
+
__enterChildren,
|
|
14
|
+
__exitChildren,
|
|
15
|
+
__staticText
|
|
16
|
+
} from "../../shared/chunk-vndfjfdy.js";
|
|
17
|
+
import"../../shared/chunk-prj7nm08.js";
|
|
18
|
+
import"../../shared/chunk-afawz764.js";
|
|
1
19
|
import {
|
|
2
20
|
_tryOnCleanup,
|
|
3
21
|
computed,
|
|
4
22
|
createContext,
|
|
23
|
+
domEffect,
|
|
5
24
|
signal,
|
|
6
25
|
useContext
|
|
7
|
-
} from "../../shared/chunk-
|
|
26
|
+
} from "../../shared/chunk-4fwcwxn6.js";
|
|
8
27
|
|
|
9
28
|
// src/auth/access-context.ts
|
|
10
29
|
var AccessContext = createContext(undefined, "@vertz/ui::AccessContext");
|
|
@@ -69,7 +88,7 @@ function createAccessEventClient(options) {
|
|
|
69
88
|
function getUrl() {
|
|
70
89
|
if (options.url)
|
|
71
90
|
return options.url;
|
|
72
|
-
if (
|
|
91
|
+
if (isBrowser()) {
|
|
73
92
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
74
93
|
return `${protocol}//${window.location.host}/api/auth/access-events`;
|
|
75
94
|
}
|
|
@@ -171,8 +190,16 @@ function handleAccessEvent(accessSet, event, flagEntitlementMap) {
|
|
|
171
190
|
case "access:limit_updated":
|
|
172
191
|
handleLimitUpdate(accessSet, current, event.entitlement, event.consumed, event.remaining, event.max);
|
|
173
192
|
break;
|
|
193
|
+
case "access:plan_assigned":
|
|
194
|
+
handlePlanAssigned(accessSet, current, event.planId);
|
|
195
|
+
break;
|
|
196
|
+
case "access:limit_reset":
|
|
197
|
+
handleLimitReset(accessSet, current, event.entitlement, event.max);
|
|
198
|
+
break;
|
|
174
199
|
case "access:role_changed":
|
|
175
200
|
case "access:plan_changed":
|
|
201
|
+
case "access:addon_attached":
|
|
202
|
+
case "access:addon_detached":
|
|
176
203
|
break;
|
|
177
204
|
}
|
|
178
205
|
}
|
|
@@ -200,6 +227,26 @@ function handleFlagToggle(accessSet, current, flag, enabled, flagEntitlementMap)
|
|
|
200
227
|
}
|
|
201
228
|
accessSet.value = { ...current, flags: newFlags, entitlements: newEntitlements };
|
|
202
229
|
}
|
|
230
|
+
function handlePlanAssigned(accessSet, current, planId) {
|
|
231
|
+
accessSet.value = { ...current, plan: planId };
|
|
232
|
+
}
|
|
233
|
+
function handleLimitReset(accessSet, current, entitlement, max) {
|
|
234
|
+
const existingEntry = current.entitlements[entitlement];
|
|
235
|
+
if (!existingEntry)
|
|
236
|
+
return;
|
|
237
|
+
const newLimit = { max, consumed: 0, remaining: max };
|
|
238
|
+
const newEntitlements = { ...current.entitlements };
|
|
239
|
+
const reasons = existingEntry.reasons.filter((r) => r !== "limit_reached");
|
|
240
|
+
const wasOnlyLimitBlocked = existingEntry.reasons.length > 0 && existingEntry.reasons.every((r) => r === "limit_reached");
|
|
241
|
+
newEntitlements[entitlement] = {
|
|
242
|
+
...existingEntry,
|
|
243
|
+
allowed: wasOnlyLimitBlocked ? true : existingEntry.allowed || reasons.length === 0,
|
|
244
|
+
reasons,
|
|
245
|
+
reason: reasons[0],
|
|
246
|
+
meta: { ...existingEntry.meta, limit: newLimit }
|
|
247
|
+
};
|
|
248
|
+
accessSet.value = { ...current, entitlements: newEntitlements };
|
|
249
|
+
}
|
|
203
250
|
function handleLimitUpdate(accessSet, current, entitlement, consumed, remaining, max) {
|
|
204
251
|
const existingEntry = current.entitlements[entitlement];
|
|
205
252
|
if (!existingEntry)
|
|
@@ -426,7 +473,7 @@ function createTokenRefresh({ onRefresh }) {
|
|
|
426
473
|
pendingOfflineRefresh = false;
|
|
427
474
|
}
|
|
428
475
|
let visibilityHandler;
|
|
429
|
-
if (
|
|
476
|
+
if (isBrowser()) {
|
|
430
477
|
visibilityHandler = () => {
|
|
431
478
|
if (document.visibilityState === "hidden") {
|
|
432
479
|
clearTimer();
|
|
@@ -437,7 +484,7 @@ function createTokenRefresh({ onRefresh }) {
|
|
|
437
484
|
document.addEventListener("visibilitychange", visibilityHandler);
|
|
438
485
|
}
|
|
439
486
|
let onlineHandler;
|
|
440
|
-
if (
|
|
487
|
+
if (isBrowser()) {
|
|
441
488
|
onlineHandler = () => {
|
|
442
489
|
if (pendingOfflineRefresh) {
|
|
443
490
|
pendingOfflineRefresh = false;
|
|
@@ -448,10 +495,10 @@ function createTokenRefresh({ onRefresh }) {
|
|
|
448
495
|
}
|
|
449
496
|
function dispose() {
|
|
450
497
|
cancel();
|
|
451
|
-
if (visibilityHandler &&
|
|
498
|
+
if (visibilityHandler && isBrowser()) {
|
|
452
499
|
document.removeEventListener("visibilitychange", visibilityHandler);
|
|
453
500
|
}
|
|
454
|
-
if (onlineHandler &&
|
|
501
|
+
if (onlineHandler && isBrowser()) {
|
|
455
502
|
window.removeEventListener("online", onlineHandler);
|
|
456
503
|
}
|
|
457
504
|
}
|
|
@@ -474,9 +521,19 @@ function AuthProvider({
|
|
|
474
521
|
flagEntitlementMap,
|
|
475
522
|
children
|
|
476
523
|
}) {
|
|
524
|
+
const router = useContext(RouterContext);
|
|
477
525
|
const userSignal = signal(null);
|
|
478
526
|
const statusSignal = signal("idle");
|
|
479
527
|
const errorSignal = signal(null);
|
|
528
|
+
const providersSignal = signal([]);
|
|
529
|
+
if (isBrowser()) {
|
|
530
|
+
setTimeout(() => {
|
|
531
|
+
fetch(`${basePath}/providers`).then((res) => res.ok ? res.json() : []).then((data) => {
|
|
532
|
+
providersSignal.value = data;
|
|
533
|
+
}).catch(() => {});
|
|
534
|
+
}, 0);
|
|
535
|
+
}
|
|
536
|
+
let deferredRefreshTimer = null;
|
|
480
537
|
const isAuthenticated = computed(() => statusSignal.value === "authenticated");
|
|
481
538
|
const isLoading = computed(() => statusSignal.value === "loading");
|
|
482
539
|
const accessSetSignal = accessControl ? signal(null) : null;
|
|
@@ -537,6 +594,10 @@ function AuthProvider({
|
|
|
537
594
|
onSuccess: handleAuthSuccess
|
|
538
595
|
});
|
|
539
596
|
const signIn = Object.assign(async (body) => {
|
|
597
|
+
if (deferredRefreshTimer) {
|
|
598
|
+
clearTimeout(deferredRefreshTimer);
|
|
599
|
+
deferredRefreshTimer = null;
|
|
600
|
+
}
|
|
540
601
|
statusSignal.value = "loading";
|
|
541
602
|
errorSignal.value = null;
|
|
542
603
|
const result = await signInMethod(body);
|
|
@@ -557,6 +618,10 @@ function AuthProvider({
|
|
|
557
618
|
onSuccess: handleAuthSuccess
|
|
558
619
|
});
|
|
559
620
|
const signUp = Object.assign(async (body) => {
|
|
621
|
+
if (deferredRefreshTimer) {
|
|
622
|
+
clearTimeout(deferredRefreshTimer);
|
|
623
|
+
deferredRefreshTimer = null;
|
|
624
|
+
}
|
|
560
625
|
statusSignal.value = "loading";
|
|
561
626
|
errorSignal.value = null;
|
|
562
627
|
const result = await signUpMethod(body);
|
|
@@ -617,7 +682,7 @@ function AuthProvider({
|
|
|
617
682
|
method: resetPasswordMethod.method,
|
|
618
683
|
meta: resetPasswordMethod.meta
|
|
619
684
|
});
|
|
620
|
-
const signOut = async () => {
|
|
685
|
+
const signOut = async (options) => {
|
|
621
686
|
tokenRefresh.cancel();
|
|
622
687
|
try {
|
|
623
688
|
await fetch(`${basePath}/signout`, {
|
|
@@ -630,9 +695,16 @@ function AuthProvider({
|
|
|
630
695
|
statusSignal.value = "unauthenticated";
|
|
631
696
|
errorSignal.value = null;
|
|
632
697
|
clearAccessSet();
|
|
633
|
-
if (
|
|
698
|
+
if (isBrowser()) {
|
|
634
699
|
delete window.__VERTZ_SESSION__;
|
|
635
700
|
}
|
|
701
|
+
if (options?.redirectTo) {
|
|
702
|
+
if (router) {
|
|
703
|
+
router.navigate({ to: options.redirectTo, replace: true }).catch(() => {});
|
|
704
|
+
} else if (typeof console !== "undefined") {
|
|
705
|
+
console.warn("[vertz] signOut({ redirectTo }) was called but no RouterContext is available. Navigation was skipped.");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
636
708
|
};
|
|
637
709
|
let refreshInFlight = null;
|
|
638
710
|
const doRefresh = async () => {
|
|
@@ -702,9 +774,10 @@ function AuthProvider({
|
|
|
702
774
|
refresh,
|
|
703
775
|
mfaChallenge,
|
|
704
776
|
forgotPassword,
|
|
705
|
-
resetPassword
|
|
777
|
+
resetPassword,
|
|
778
|
+
providers: providersSignal
|
|
706
779
|
};
|
|
707
|
-
if (
|
|
780
|
+
if (isBrowser()) {
|
|
708
781
|
if (window.__VERTZ_SESSION__?.user) {
|
|
709
782
|
const session = window.__VERTZ_SESSION__;
|
|
710
783
|
userSignal.value = session.user;
|
|
@@ -713,11 +786,14 @@ function AuthProvider({
|
|
|
713
786
|
tokenRefresh.schedule(session.expiresAt);
|
|
714
787
|
}
|
|
715
788
|
} else {
|
|
716
|
-
|
|
789
|
+
deferredRefreshTimer = setTimeout(() => {
|
|
790
|
+
deferredRefreshTimer = null;
|
|
791
|
+
refresh();
|
|
792
|
+
}, 0);
|
|
717
793
|
}
|
|
718
794
|
}
|
|
719
795
|
if (accessControl && accessSetSignal && accessLoadingSignal) {
|
|
720
|
-
if (
|
|
796
|
+
if (isBrowser() && window.__VERTZ_ACCESS_SET__ && typeof window.__VERTZ_ACCESS_SET__.entitlements === "object" && window.__VERTZ_ACCESS_SET__.entitlements !== null) {
|
|
721
797
|
accessSetSignal.value = window.__VERTZ_ACCESS_SET__;
|
|
722
798
|
accessLoadingSignal.value = false;
|
|
723
799
|
}
|
|
@@ -753,18 +829,146 @@ function AuthGate({ fallback, children }) {
|
|
|
753
829
|
function createAccessProvider() {
|
|
754
830
|
const accessSet = signal(null);
|
|
755
831
|
const loading = signal(true);
|
|
756
|
-
if (
|
|
832
|
+
if (isBrowser() && window.__VERTZ_ACCESS_SET__ && typeof window.__VERTZ_ACCESS_SET__.entitlements === "object" && window.__VERTZ_ACCESS_SET__.entitlements !== null) {
|
|
757
833
|
accessSet.value = window.__VERTZ_ACCESS_SET__;
|
|
758
834
|
loading.value = false;
|
|
759
835
|
}
|
|
760
836
|
return { accessSet, loading };
|
|
761
837
|
}
|
|
838
|
+
// src/auth/provider-icons.ts
|
|
839
|
+
var icons = {
|
|
840
|
+
github: (size) => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12Z"/></svg>`,
|
|
841
|
+
google: (size) => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1Z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23Z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62Z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53Z"/></svg>`,
|
|
842
|
+
discord: (size) => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03ZM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418Zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418Z"/></svg>`,
|
|
843
|
+
apple: (size) => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor"><path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09ZM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.32 2.32-1.55 4.25-3.74 4.25Z"/></svg>`,
|
|
844
|
+
microsoft: (size) => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24"><rect fill="#F25022" x="1" y="1" width="10" height="10"/><rect fill="#7FBA00" x="13" y="1" width="10" height="10"/><rect fill="#00A4EF" x="1" y="13" width="10" height="10"/><rect fill="#FFB900" x="13" y="13" width="10" height="10"/></svg>`,
|
|
845
|
+
twitter: (size) => `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>`
|
|
846
|
+
};
|
|
847
|
+
function fallbackIcon(size) {
|
|
848
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>`;
|
|
849
|
+
}
|
|
850
|
+
function getProviderIcon(providerId, size) {
|
|
851
|
+
const iconFn = icons[providerId];
|
|
852
|
+
return iconFn ? iconFn(size) : fallbackIcon(size);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// src/auth/oauth-button.ts
|
|
856
|
+
var DANGEROUS_SCHEMES = ["javascript:", "data:", "vbscript:"];
|
|
857
|
+
function isSafeUrl(url) {
|
|
858
|
+
const normalized = url.replace(/\s/g, "").toLowerCase();
|
|
859
|
+
if (normalized.startsWith("//"))
|
|
860
|
+
return false;
|
|
861
|
+
for (const scheme of DANGEROUS_SCHEMES) {
|
|
862
|
+
if (normalized.startsWith(scheme))
|
|
863
|
+
return false;
|
|
864
|
+
}
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
function OAuthButton({ provider, label, iconOnly, _providers }) {
|
|
868
|
+
const providers = _providers ?? useAuth().providers;
|
|
869
|
+
const providerInfo = providers.find((p) => p.id === provider);
|
|
870
|
+
if (!providerInfo) {
|
|
871
|
+
return __element("span");
|
|
872
|
+
}
|
|
873
|
+
const safeAuthUrl = isSafeUrl(providerInfo.authUrl) ? providerInfo.authUrl : "#";
|
|
874
|
+
const props = { type: "button" };
|
|
875
|
+
if (iconOnly) {
|
|
876
|
+
props["aria-label"] = `Continue with ${providerInfo.name}`;
|
|
877
|
+
}
|
|
878
|
+
const el = __element("button", props);
|
|
879
|
+
__on(el, "click", () => {
|
|
880
|
+
window.location.href = safeAuthUrl;
|
|
881
|
+
});
|
|
882
|
+
__enterChildren(el);
|
|
883
|
+
const iconSpan = __element("span");
|
|
884
|
+
iconSpan.innerHTML = getProviderIcon(provider, 20);
|
|
885
|
+
__append(el, iconSpan);
|
|
886
|
+
if (!iconOnly) {
|
|
887
|
+
const text = label ?? `Continue with ${providerInfo.name}`;
|
|
888
|
+
const textSpan = __element("span");
|
|
889
|
+
__enterChildren(textSpan);
|
|
890
|
+
__append(textSpan, __staticText(text));
|
|
891
|
+
__exitChildren();
|
|
892
|
+
__append(el, textSpan);
|
|
893
|
+
}
|
|
894
|
+
__exitChildren();
|
|
895
|
+
return el;
|
|
896
|
+
}
|
|
897
|
+
// src/auth/oauth-buttons.ts
|
|
898
|
+
function OAuthButtons({ _providers } = {}) {
|
|
899
|
+
const providers = _providers ?? useAuth().providers;
|
|
900
|
+
const container = __element("div");
|
|
901
|
+
__enterChildren(container);
|
|
902
|
+
for (const provider of providers) {
|
|
903
|
+
const button = OAuthButton({
|
|
904
|
+
provider: provider.id,
|
|
905
|
+
_providers: providers
|
|
906
|
+
});
|
|
907
|
+
__append(container, button);
|
|
908
|
+
}
|
|
909
|
+
__exitChildren();
|
|
910
|
+
return container;
|
|
911
|
+
}
|
|
912
|
+
// src/auth/protected-route.ts
|
|
913
|
+
var __DEV__2 = typeof process !== "undefined" && true;
|
|
914
|
+
function ProtectedRoute({
|
|
915
|
+
loginPath = "/login",
|
|
916
|
+
fallback,
|
|
917
|
+
children,
|
|
918
|
+
requires,
|
|
919
|
+
forbidden,
|
|
920
|
+
returnTo = true
|
|
921
|
+
}) {
|
|
922
|
+
const ctx = useContext(AuthContext);
|
|
923
|
+
if (!ctx) {
|
|
924
|
+
if (__DEV__2) {
|
|
925
|
+
console.warn("ProtectedRoute used without AuthProvider — rendering children unprotected");
|
|
926
|
+
}
|
|
927
|
+
return typeof children === "function" ? children() : children;
|
|
928
|
+
}
|
|
929
|
+
const router = useContext(RouterContext);
|
|
930
|
+
const checks = requires?.map((e) => can(e));
|
|
931
|
+
const allAllowed = computed(() => !checks || checks.every((c) => c.allowed.value));
|
|
932
|
+
const isResolved = computed(() => {
|
|
933
|
+
const status = ctx.status;
|
|
934
|
+
return status !== "idle" && status !== "loading";
|
|
935
|
+
});
|
|
936
|
+
const shouldRedirect = computed(() => {
|
|
937
|
+
if (!isResolved.value)
|
|
938
|
+
return false;
|
|
939
|
+
return !ctx.isAuthenticated;
|
|
940
|
+
});
|
|
941
|
+
if (router) {
|
|
942
|
+
domEffect(() => {
|
|
943
|
+
if (shouldRedirect.value) {
|
|
944
|
+
const search = returnTo && isBrowser() ? `?returnTo=${encodeURIComponent(window.location.pathname + window.location.search)}` : "";
|
|
945
|
+
router.navigate({ to: `${loginPath}${search}`, replace: true });
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
return computed(() => {
|
|
950
|
+
if (!isResolved.value) {
|
|
951
|
+
return fallback ? fallback() : null;
|
|
952
|
+
}
|
|
953
|
+
if (shouldRedirect.value) {
|
|
954
|
+
return fallback ? fallback() : null;
|
|
955
|
+
}
|
|
956
|
+
if (!allAllowed.value) {
|
|
957
|
+
return forbidden ? forbidden() : null;
|
|
958
|
+
}
|
|
959
|
+
return typeof children === "function" ? children() : children;
|
|
960
|
+
});
|
|
961
|
+
}
|
|
762
962
|
export {
|
|
763
963
|
useAuth,
|
|
764
964
|
useAccessContext,
|
|
965
|
+
getProviderIcon,
|
|
765
966
|
createAccessProvider,
|
|
766
967
|
createAccessEventClient,
|
|
767
968
|
can,
|
|
969
|
+
ProtectedRoute,
|
|
970
|
+
OAuthButtons,
|
|
971
|
+
OAuthButton,
|
|
768
972
|
AuthProvider,
|
|
769
973
|
AuthGate,
|
|
770
974
|
AuthContext,
|