@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.
Files changed (32) hide show
  1. package/README.md +49 -0
  2. package/dist/shared/chunk-14eqne2a.js +10 -0
  3. package/dist/shared/{chunk-dksg08fq.js → chunk-1rxa2fz4.js} +2 -8
  4. package/dist/shared/{chunk-8hsz5y4a.js → chunk-4fwcwxn6.js} +14 -4
  5. package/dist/shared/{chunk-hw67ckr3.js → chunk-4mtn7af6.js} +230 -19
  6. package/dist/shared/{chunk-2sth83bd.js → chunk-6jyt4ycw.js} +67 -2
  7. package/dist/shared/{chunk-83g4h38e.js → chunk-6wd36w21.js} +1 -0
  8. package/dist/shared/{chunk-h89w580h.js → chunk-afawz764.js} +1 -1
  9. package/dist/shared/{chunk-nn9v1zmk.js → chunk-b0qqqk03.js} +86 -21
  10. package/dist/shared/{chunk-c9xxsrat.js → chunk-dhehvmj0.js} +179 -10
  11. package/dist/shared/{chunk-mj7b4t40.js → chunk-fkbgbf3n.js} +98 -11
  12. package/dist/shared/{chunk-c30eg6wn.js → chunk-j09yyh34.js} +79 -6
  13. package/dist/shared/chunk-mtsvrj9e.js +23 -0
  14. package/dist/shared/chunk-pnv25zep.js +7 -0
  15. package/dist/shared/{chunk-j6qyxfdc.js → chunk-vndfjfdy.js} +3 -3
  16. package/dist/src/auth/public.d.ts +69 -9
  17. package/dist/src/auth/public.js +217 -13
  18. package/dist/src/css/public.d.ts +110 -2
  19. package/dist/src/css/public.js +8 -4
  20. package/dist/src/form/public.d.ts +33 -7
  21. package/dist/src/form/public.js +2 -2
  22. package/dist/src/index.d.ts +311 -20
  23. package/dist/src/index.js +161 -14
  24. package/dist/src/internals.d.ts +141 -5
  25. package/dist/src/internals.js +17 -9
  26. package/dist/src/query/public.js +4 -3
  27. package/dist/src/router/public.d.ts +39 -9
  28. package/dist/src/router/public.js +17 -11
  29. package/dist/src/test/index.d.ts +26 -23
  30. package/dist/src/test/index.js +5 -4
  31. package/package.json +3 -3
  32. package/reactivity.json +1 -11
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  computed,
3
3
  signal
4
- } from "./chunk-8hsz5y4a.js";
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
- result[key] = coerce ? coerceValue(value) : value;
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 initialObj = typeof options?.initial === "function" ? options.initial() : options?.initial;
106
- const initialValue = initialObj?.[name];
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 getOrCreateField(prop);
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 };
@@ -0,0 +1,7 @@
1
+ // src/dom/events.ts
2
+ function __on(el, event, handler) {
3
+ el.addEventListener(event, handler);
4
+ return () => el.removeEventListener(event, handler);
5
+ }
6
+
7
+ export { __on };
@@ -6,10 +6,10 @@ import {
6
6
  import {
7
7
  getAdapter,
8
8
  isRenderNode
9
- } from "./chunk-h89w580h.js";
9
+ } from "./chunk-afawz764.js";
10
10
  import {
11
11
  domEffect
12
- } from "./chunk-8hsz5y4a.js";
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: string, entity?: {
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
- export { useAuth, useAccessContext, createAccessProvider, createAccessEventClient, can, User, SignUpInput, SignInInput, ResetInput, MfaInput, ForgotInput, DenialReason, DenialMeta, ClientAccessEvent, AuthStatus, AuthResponse, AuthProviderProps, AuthProvider, AuthGateProps, AuthGate, AuthErrorCode, AuthContextValue, AuthContext, AuthClientError, AccessSet, AccessGateProps, AccessGate, AccessEventClientOptions, AccessEventClient, AccessContextValue, AccessContext, AccessCheckData, AccessCheck };
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 };
@@ -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-8hsz5y4a.js";
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 (typeof window !== "undefined") {
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 (typeof document !== "undefined") {
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 (typeof window !== "undefined") {
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 && typeof document !== "undefined") {
498
+ if (visibilityHandler && isBrowser()) {
452
499
  document.removeEventListener("visibilitychange", visibilityHandler);
453
500
  }
454
- if (onlineHandler && typeof window !== "undefined") {
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 (typeof window !== "undefined") {
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 (typeof window !== "undefined") {
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
- statusSignal.value = "unauthenticated";
789
+ deferredRefreshTimer = setTimeout(() => {
790
+ deferredRefreshTimer = null;
791
+ refresh();
792
+ }, 0);
717
793
  }
718
794
  }
719
795
  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) {
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 (typeof window !== "undefined" && window.__VERTZ_ACCESS_SET__ && typeof window.__VERTZ_ACCESS_SET__.entitlements === "object" && window.__VERTZ_ACCESS_SET__.entitlements !== null) {
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,