@fluixi/auth 1.0.0-alpha.83 → 1.0.0-alpha.85

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/dist/client.cjs CHANGED
@@ -1 +1,136 @@
1
- "use strict";var f=Object.defineProperty;var b=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var x=Object.prototype.hasOwnProperty;var O=(s,n)=>{for(var r in n)f(s,r,{get:n[r],enumerable:!0})},Q=(s,n,r,u)=>{if(n&&typeof n=="object"||typeof n=="function")for(let t of y(n))!x.call(s,t)&&t!==r&&f(s,t,{get:()=>n[t],enumerable:!(u=b(n,t))||u.enumerable});return s};var v=s=>Q(f({},"__esModule",{value:!0}),s);var L={};O(L,{createAuthHooks:()=>D});module.exports=v(L);var o=require("@fluixi/core"),k=require("@fluixi/dom");var h="auth";function D(s){let n=s.session,r=(0,o.createIsomorphicStore)("auth",{server:()=>(0,k.getLocals)()[h]??{user:null,session:null},client:async()=>n?{user:n.user(),session:null}:P(await s.client.getSession()),fallback:{user:null,session:null}});if(n){let e=()=>r.set({user:n.user(),session:null});e(),(0,o.createRoot)(()=>(0,o.createEffect)(e))}let u=async()=>{r.set(n?{user:n.user(),session:null}:P(await s.client.getSession()))},t=n?((...e)=>n.login?.(...e)??Promise.resolve()):async e=>{await s.client.signIn.email(e),await u()},g=n?()=>n.logout?.()??Promise.resolve():async()=>{await s.client.signOut(),r.set({user:null,session:null})},a=e=>(s.navigate??(i=>o.env.window?.location.assign(i)))(e),p=()=>n?.permissions?.(),l=e=>n?.can?.(e)??!1,c=()=>{if(n?.roles)return n.roles();let e=r.fields.user()?.role;return e?[e]:[]},d=e=>!(e?.role&&!c().includes(e.role)||e?.roles&&!e.roles.some(i=>c().includes(i))||e?.permission!==void 0&&!l(e.permission));function w(e){return()=>l(e.perform)?e.children:e.fallback??null}function U(e){return()=>r.fields.user()&&d(e)?e.children:e.fallback??null}function A(e,i){return S=>{let m=r.fields.user();if(i?.guestOnly){if(m)return a(s.homePath??"/"),null}else{if(!m)return a(s.loginPath??"/login"),null;if(!d(i))return a(s.forbiddenPath??"/403"),null}return e(S)}}return{useUser:()=>r.fields.user,useSession:()=>r.fields.session,useAuth:()=>({user:r.fields.user,session:r.fields.session,isAuthenticated:()=>r.fields.user()!==null,roles:c,can:l,signIn:t,signOut:g}),useSignIn:()=>t,useSignOut:()=>g,usePermissions:()=>p,useCan:()=>l,Can:w,Protect:U,refresh:u,protectedRoute:A,store:r}}function P(s){return{user:s.data?.user??null,session:s.data?.session??null}}
1
+ /*! @fluixi/auth v1.0.0-alpha.85 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/client.ts
22
+ var client_exports = {};
23
+ __export(client_exports, {
24
+ createAuthHooks: () => createAuthHooks
25
+ });
26
+ module.exports = __toCommonJS(client_exports);
27
+ var import_core = require("@fluixi/core");
28
+ var import_dom = require("@fluixi/dom");
29
+
30
+ // src/types.ts
31
+ var AUTH_LOCALS_KEY = "auth";
32
+
33
+ // src/client.ts
34
+ function createAuthHooks(options) {
35
+ const sess = options.session;
36
+ const store = (0, import_core.createIsomorphicStore)("auth", {
37
+ // Server: read what authMiddleware put on locals → serialized for the client to seed.
38
+ server: () => (0, import_dom.getLocals)()[AUTH_LOCALS_KEY] ?? { user: null, session: null },
39
+ // SPA cold load: from the provided session, else the Better Auth client.
40
+ client: async () => sess ? { user: sess.user(), session: null } : normalize(await options.client.getSession()),
41
+ fallback: { user: null, session: null }
42
+ });
43
+ if (sess) {
44
+ const apply = () => store.set({ user: sess.user(), session: null });
45
+ apply();
46
+ (0, import_core.createRoot)(() => (0, import_core.createEffect)(apply));
47
+ }
48
+ const refresh = async () => {
49
+ store.set(sess ? { user: sess.user(), session: null } : normalize(await options.client.getSession()));
50
+ };
51
+ const signIn = sess ? ((...args) => sess.login?.(...args) ?? Promise.resolve()) : async (creds) => {
52
+ await options.client.signIn.email(creds);
53
+ await refresh();
54
+ };
55
+ const signOut = sess ? () => sess.logout?.() ?? Promise.resolve() : async () => {
56
+ await options.client.signOut();
57
+ store.set({ user: null, session: null });
58
+ };
59
+ const go = (to, opts) => {
60
+ const fromRoute = typeof opts?.handler === "function" ? opts.handler() : void 0;
61
+ const fromHooks = typeof options.navigate === "function" ? options.navigate : typeof options.navigate?.handler === "function" ? (
62
+ // Called, not passed along. Handing back the factory made `fn(to)` build a
63
+ // navigate function and return it, so the redirect silently did nothing.
64
+ options.navigate.handler()
65
+ ) : void 0;
66
+ const fn = fromRoute ?? fromHooks;
67
+ if (fn) {
68
+ const navigateOptions = opts?.options ?? (typeof options.navigate === "object" ? options.navigate?.options : void 0);
69
+ if (navigateOptions) fn(to, navigateOptions);
70
+ else fn(to);
71
+ return;
72
+ }
73
+ import_core.env.window?.location.assign(to);
74
+ };
75
+ const permissions = () => sess?.permissions?.();
76
+ const can = (query) => sess?.can?.(query) ?? false;
77
+ const roleList = () => {
78
+ if (sess?.roles) return sess.roles();
79
+ const r = store.fields.user()?.role;
80
+ return r ? [r] : [];
81
+ };
82
+ const passesRole = (opts) => {
83
+ if (opts?.role && !roleList().includes(opts.role)) return false;
84
+ if (opts?.roles && !opts.roles.some((r) => roleList().includes(r))) return false;
85
+ if (opts?.permission !== void 0 && !can(opts.permission)) return false;
86
+ return true;
87
+ };
88
+ function Can(props) {
89
+ return () => can(props.perform) ? props.children : props.fallback ?? null;
90
+ }
91
+ function Protect(props) {
92
+ return () => {
93
+ if (!store.fields.user()) return props.fallback ?? null;
94
+ return passesRole(props) ? props.children : props.fallback ?? null;
95
+ };
96
+ }
97
+ function protectedRoute(Comp, opts) {
98
+ return (props) => {
99
+ const user = store.fields.user();
100
+ if (opts?.guestOnly) {
101
+ if (user) return go(options.homePath ?? "/", opts?.navigator), null;
102
+ } else {
103
+ if (!user) return go(options.loginPath ?? "/login", opts?.navigator), null;
104
+ if (!passesRole(opts)) return go(options.forbiddenPath ?? "/403"), null;
105
+ }
106
+ return Comp(props);
107
+ };
108
+ }
109
+ return {
110
+ useUser: () => store.fields.user,
111
+ useSession: () => store.fields.session,
112
+ useAuth: () => ({
113
+ user: store.fields.user,
114
+ session: store.fields.session,
115
+ isAuthenticated: () => store.fields.user() !== null,
116
+ roles: roleList,
117
+ can,
118
+ signIn,
119
+ signOut
120
+ }),
121
+ useSignIn: () => signIn,
122
+ useSignOut: () => signOut,
123
+ /** Reactive accessor for the active permission set (UI affordance only). */
124
+ usePermissions: () => permissions,
125
+ /** Returns `can(check)`: `false` unless the session provides RBAC. */
126
+ useCan: () => can,
127
+ Can,
128
+ Protect,
129
+ refresh,
130
+ protectedRoute,
131
+ store
132
+ };
133
+ }
134
+ function normalize(r) {
135
+ return { user: r.data?.user ?? null, session: r.data?.session ?? null };
136
+ }
package/dist/client.d.ts CHANGED
@@ -5,12 +5,13 @@ export interface ProtectedRouteOptions<Q = (p: any) => boolean> {
5
5
  role?: string;
6
6
  /** Require ANY of these roles. */
7
7
  roles?: string[];
8
- /** Require a permission your access matcher's query (default: a predicate). */
8
+ /** Require a permission: your access matcher's query (default: a predicate). */
9
9
  permission?: Q;
10
+ navigator?: Partial<Navigator>;
10
11
  }
11
12
  /**
12
13
  * Any reactive session with a `user` accessor (e.g. @fluixi/session's createSession).
13
- * The RBAC accessors (`roles` / `permissions` / `can`) are optional provide them (as
14
+ * The RBAC accessors (`roles` / `permissions` / `can`) are optional, provide them (as
14
15
  * createSession's `access` config does) to unlock `<Can>`, `usePermissions`, and
15
16
  * permission-gated routes. They are a UI affordance only; the server enforces authz.
16
17
  */
@@ -22,13 +23,24 @@ export interface SessionLike<U = AuthUser, P = unknown, Q = (p: P) => boolean> {
22
23
  permissions?: () => P | undefined;
23
24
  can?: (query: Q) => boolean;
24
25
  }
26
+ export interface NavigateOptions {
27
+ /** Replace the current history entry instead of pushing a new one. */
28
+ replace?: boolean;
29
+ /** Opaque history state to attach. */
30
+ state?: unknown;
31
+ transition?: unknown;
32
+ }
33
+ export interface Navigator {
34
+ handler: () => (to: string, opts?: any) => void;
35
+ options?: NavigateOptions;
36
+ }
25
37
  export interface AuthHooksOptions<U = AuthUser, P = unknown, Q = (p: P) => boolean> {
26
38
  /** A Better Auth client (`createAuthClient(...)`). Mutually exclusive with `session`. */
27
39
  client?: BetterAuthClient;
28
- /** An existing reactive session the framework syncs it into the hooks for you. */
40
+ /** An existing reactive session: the framework syncs it into the hooks for you. */
29
41
  session?: SessionLike<U, P, Q>;
30
42
  /** Navigate for `protectedRoute` redirects (default: `window.location.assign`). */
31
- navigate?: (to: string) => void;
43
+ navigate?: ((to: string, opts?: NavigateOptions) => void) | Navigator;
32
44
  loginPath?: string;
33
45
  homePath?: string;
34
46
  forbiddenPath?: string;
@@ -57,7 +69,7 @@ export declare function createAuthHooks<U = AuthUser, P = unknown, Q = (p: P) =>
57
69
  useSignOut: () => () => Promise<unknown>;
58
70
  /** Reactive accessor for the active permission set (UI affordance only). */
59
71
  usePermissions: () => () => P | undefined;
60
- /** Returns `can(check)` `false` unless the session provides RBAC. */
72
+ /** Returns `can(check)`: `false` unless the session provides RBAC. */
61
73
  useCan: () => (query: Q) => boolean;
62
74
  Can: (props: {
63
75
  perform: Q;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js CHANGED
@@ -1,15 +1,15 @@
1
1
  /**
2
- * @fluixi/auth/client the reactive client hooks.
2
+ * @fluixi/auth/client: the reactive client hooks.
3
3
  *
4
4
  * The session is an isomorphic store: resolved on the server from `locals.auth`
5
5
  * (serialized into the page) and seeded synchronously on the client → no auth flash, no
6
6
  * post-hydration refetch. Two ways to drive it:
7
- * • `{ client }` a Better Auth client.
8
- * • `{ session }` an existing reactive session (e.g. @fluixi/session's createSession or
7
+ * • `{ client }`: a Better Auth client.
8
+ * • `{ session }`: an existing reactive session (e.g. @fluixi/session's createSession or
9
9
  * any `{ user(), login?, logout? }`). The framework OWNS the reactive sync, so apps stop
10
10
  * hand-writing a `createEffect` to mirror their store into the auth hooks.
11
11
  *
12
- * Client-only deps: fluixi + @fluixi/dom's pure `getLocals` never @fluixi/server.
12
+ * Client-only deps: fluixi + @fluixi/dom's pure `getLocals`, never @fluixi/server.
13
13
  */
14
14
  import { createIsomorphicStore, createEffect, createRoot, env } from '@fluixi/core';
15
15
  import { getLocals } from '@fluixi/dom';
@@ -46,7 +46,42 @@ export function createAuthHooks(options) {
46
46
  await options.client.signOut();
47
47
  store.set({ user: null, session: null });
48
48
  };
49
- const go = (to) => (options.navigate ?? ((t) => env.window?.location.assign(t)))(to);
49
+ /**
50
+ * Send the visitor somewhere else.
51
+ *
52
+ * The route's own `navigator` wins, then the one given to `createAuthHooks`, then a
53
+ * document load. A handler is a factory rather than a function: `useNavigate` has to
54
+ * run inside the component being rendered, so it is called here, at the redirect,
55
+ * instead of when the hooks were built.
56
+ *
57
+ * Without a handler this assigns `location`, which loads a document and rebuilds the
58
+ * whole app. That is right for an app with no client router and wrong for one that
59
+ * has it, which is what the option exists to fix.
60
+ */
61
+ const go = (to, opts) => {
62
+ const fromRoute = typeof opts?.handler === 'function' ? opts.handler() : undefined;
63
+ const fromHooks = typeof options.navigate === 'function'
64
+ ? options.navigate
65
+ : typeof options.navigate?.handler === 'function'
66
+ ? // Called, not passed along. Handing back the factory made `fn(to)` build a
67
+ // navigate function and return it, so the redirect silently did nothing.
68
+ options.navigate.handler()
69
+ : undefined;
70
+ const fn = fromRoute ?? fromHooks;
71
+ if (fn) {
72
+ const navigateOptions = opts?.options ??
73
+ (typeof options.navigate === 'object' ? options.navigate?.options : undefined);
74
+ // Only when there are some. Deciding on the handler's arity instead dropped them
75
+ // for anything declaring the parameter optional, which `useNavigate` does; passing
76
+ // `undefined` regardless would change the call every handler sees.
77
+ if (navigateOptions)
78
+ fn(to, navigateOptions);
79
+ else
80
+ fn(to);
81
+ return;
82
+ }
83
+ env.window?.location.assign(to);
84
+ };
50
85
  // ── RBAC surface (driven by the bound session's access config) ────────────────
51
86
  const permissions = () => sess?.permissions?.();
52
87
  const can = (query) => sess?.can?.(query) ?? false;
@@ -82,11 +117,11 @@ export function createAuthHooks(options) {
82
117
  const user = store.fields.user();
83
118
  if (opts?.guestOnly) {
84
119
  if (user)
85
- return go(options.homePath ?? '/'), null;
120
+ return go(options.homePath ?? '/', opts?.navigator), null;
86
121
  }
87
122
  else {
88
123
  if (!user)
89
- return go(options.loginPath ?? '/login'), null;
124
+ return go(options.loginPath ?? '/login', opts?.navigator), null;
90
125
  if (!passesRole(opts))
91
126
  return go(options.forbiddenPath ?? '/403'), null;
92
127
  }
@@ -109,7 +144,7 @@ export function createAuthHooks(options) {
109
144
  useSignOut: () => signOut,
110
145
  /** Reactive accessor for the active permission set (UI affordance only). */
111
146
  usePermissions: () => permissions,
112
- /** Returns `can(check)` `false` unless the session provides RBAC. */
147
+ /** Returns `can(check)`: `false` unless the session provides RBAC. */
113
148
  useCan: () => can,
114
149
  Can,
115
150
  Protect,
package/dist/client.mjs CHANGED
@@ -1 +1,116 @@
1
- import{createIsomorphicStore as U,createEffect as A,createRoot as S,env as b}from"@fluixi/core";import{getLocals as y}from"@fluixi/dom";var d="auth";function D(s){let n=s.session,r=U("auth",{server:()=>y()[d]??{user:null,session:null},client:async()=>n?{user:n.user(),session:null}:m(await s.client.getSession()),fallback:{user:null,session:null}});if(n){let e=()=>r.set({user:n.user(),session:null});e(),S(()=>A(e))}let l=async()=>{r.set(n?{user:n.user(),session:null}:m(await s.client.getSession()))},a=n?((...e)=>n.login?.(...e)??Promise.resolve()):async e=>{await s.client.signIn.email(e),await l()},c=n?()=>n.logout?.()??Promise.resolve():async()=>{await s.client.signOut(),r.set({user:null,session:null})},i=e=>(s.navigate??(t=>b.window?.location.assign(t)))(e),h=()=>n?.permissions?.(),o=e=>n?.can?.(e)??!1,u=()=>{if(n?.roles)return n.roles();let e=r.fields.user()?.role;return e?[e]:[]},f=e=>!(e?.role&&!u().includes(e.role)||e?.roles&&!e.roles.some(t=>u().includes(t))||e?.permission!==void 0&&!o(e.permission));function P(e){return()=>o(e.perform)?e.children:e.fallback??null}function k(e){return()=>r.fields.user()&&f(e)?e.children:e.fallback??null}function p(e,t){return w=>{let g=r.fields.user();if(t?.guestOnly){if(g)return i(s.homePath??"/"),null}else{if(!g)return i(s.loginPath??"/login"),null;if(!f(t))return i(s.forbiddenPath??"/403"),null}return e(w)}}return{useUser:()=>r.fields.user,useSession:()=>r.fields.session,useAuth:()=>({user:r.fields.user,session:r.fields.session,isAuthenticated:()=>r.fields.user()!==null,roles:u,can:o,signIn:a,signOut:c}),useSignIn:()=>a,useSignOut:()=>c,usePermissions:()=>h,useCan:()=>o,Can:P,Protect:k,refresh:l,protectedRoute:p,store:r}}function m(s){return{user:s.data?.user??null,session:s.data?.session??null}}export{D as createAuthHooks};
1
+ /*! @fluixi/auth v1.0.0-alpha.85 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
2
+
3
+ // src/client.ts
4
+ import { createIsomorphicStore, createEffect, createRoot, env } from "@fluixi/core";
5
+ import { getLocals } from "@fluixi/dom";
6
+
7
+ // src/types.ts
8
+ var AUTH_LOCALS_KEY = "auth";
9
+
10
+ // src/client.ts
11
+ function createAuthHooks(options) {
12
+ const sess = options.session;
13
+ const store = createIsomorphicStore("auth", {
14
+ // Server: read what authMiddleware put on locals → serialized for the client to seed.
15
+ server: () => getLocals()[AUTH_LOCALS_KEY] ?? { user: null, session: null },
16
+ // SPA cold load: from the provided session, else the Better Auth client.
17
+ client: async () => sess ? { user: sess.user(), session: null } : normalize(await options.client.getSession()),
18
+ fallback: { user: null, session: null }
19
+ });
20
+ if (sess) {
21
+ const apply = () => store.set({ user: sess.user(), session: null });
22
+ apply();
23
+ createRoot(() => createEffect(apply));
24
+ }
25
+ const refresh = async () => {
26
+ store.set(sess ? { user: sess.user(), session: null } : normalize(await options.client.getSession()));
27
+ };
28
+ const signIn = sess ? ((...args) => sess.login?.(...args) ?? Promise.resolve()) : async (creds) => {
29
+ await options.client.signIn.email(creds);
30
+ await refresh();
31
+ };
32
+ const signOut = sess ? () => sess.logout?.() ?? Promise.resolve() : async () => {
33
+ await options.client.signOut();
34
+ store.set({ user: null, session: null });
35
+ };
36
+ const go = (to, opts) => {
37
+ const fromRoute = typeof opts?.handler === "function" ? opts.handler() : void 0;
38
+ const fromHooks = typeof options.navigate === "function" ? options.navigate : typeof options.navigate?.handler === "function" ? (
39
+ // Called, not passed along. Handing back the factory made `fn(to)` build a
40
+ // navigate function and return it, so the redirect silently did nothing.
41
+ options.navigate.handler()
42
+ ) : void 0;
43
+ const fn = fromRoute ?? fromHooks;
44
+ if (fn) {
45
+ const navigateOptions = opts?.options ?? (typeof options.navigate === "object" ? options.navigate?.options : void 0);
46
+ if (navigateOptions) fn(to, navigateOptions);
47
+ else fn(to);
48
+ return;
49
+ }
50
+ env.window?.location.assign(to);
51
+ };
52
+ const permissions = () => sess?.permissions?.();
53
+ const can = (query) => sess?.can?.(query) ?? false;
54
+ const roleList = () => {
55
+ if (sess?.roles) return sess.roles();
56
+ const r = store.fields.user()?.role;
57
+ return r ? [r] : [];
58
+ };
59
+ const passesRole = (opts) => {
60
+ if (opts?.role && !roleList().includes(opts.role)) return false;
61
+ if (opts?.roles && !opts.roles.some((r) => roleList().includes(r))) return false;
62
+ if (opts?.permission !== void 0 && !can(opts.permission)) return false;
63
+ return true;
64
+ };
65
+ function Can(props) {
66
+ return () => can(props.perform) ? props.children : props.fallback ?? null;
67
+ }
68
+ function Protect(props) {
69
+ return () => {
70
+ if (!store.fields.user()) return props.fallback ?? null;
71
+ return passesRole(props) ? props.children : props.fallback ?? null;
72
+ };
73
+ }
74
+ function protectedRoute(Comp, opts) {
75
+ return (props) => {
76
+ const user = store.fields.user();
77
+ if (opts?.guestOnly) {
78
+ if (user) return go(options.homePath ?? "/", opts?.navigator), null;
79
+ } else {
80
+ if (!user) return go(options.loginPath ?? "/login", opts?.navigator), null;
81
+ if (!passesRole(opts)) return go(options.forbiddenPath ?? "/403"), null;
82
+ }
83
+ return Comp(props);
84
+ };
85
+ }
86
+ return {
87
+ useUser: () => store.fields.user,
88
+ useSession: () => store.fields.session,
89
+ useAuth: () => ({
90
+ user: store.fields.user,
91
+ session: store.fields.session,
92
+ isAuthenticated: () => store.fields.user() !== null,
93
+ roles: roleList,
94
+ can,
95
+ signIn,
96
+ signOut
97
+ }),
98
+ useSignIn: () => signIn,
99
+ useSignOut: () => signOut,
100
+ /** Reactive accessor for the active permission set (UI affordance only). */
101
+ usePermissions: () => permissions,
102
+ /** Returns `can(check)`: `false` unless the session provides RBAC. */
103
+ useCan: () => can,
104
+ Can,
105
+ Protect,
106
+ refresh,
107
+ protectedRoute,
108
+ store
109
+ };
110
+ }
111
+ function normalize(r) {
112
+ return { user: r.data?.user ?? null, session: r.data?.session ?? null };
113
+ }
114
+ export {
115
+ createAuthHooks
116
+ };
package/dist/cookies.cjs CHANGED
@@ -1 +1,72 @@
1
- "use strict";var r=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var l=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var f=(t,n)=>{for(var o in n)r(t,o,{get:n[o],enumerable:!0})},m=(t,n,o,e)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of l(n))!g.call(t,i)&&i!==o&&r(t,i,{get:()=>n[i],enumerable:!(e=c(n,i))||e.enumerable});return t};var d=t=>m(r({},"__esModule",{value:!0}),t);var h={};f(h,{deleteCookie:()=>x,getCookie:()=>k,getCookies:()=>C,parseCookies:()=>s,serializeCookie:()=>u});module.exports=d(h);var a=require("@fluixi/server");function s(t){let n={};if(!t)return n;for(let o of t.split(";")){let e=o.indexOf("=");if(e<0)continue;let i=o.slice(0,e).trim();i&&(n[i]=decodeURIComponent(o.slice(e+1).trim()))}return n}function u(t,n,o={}){let e={path:"/",httpOnly:!0,secure:!0,sameSite:"lax",...o},i=`${t}=${encodeURIComponent(n)}`;return e.maxAge!=null&&(i+=`; Max-Age=${Math.floor(e.maxAge)}`),e.expires&&(i+=`; Expires=${e.expires.toUTCString()}`),e.domain&&(i+=`; Domain=${e.domain}`),i+=`; Path=${e.path}`,e.httpOnly&&(i+="; HttpOnly"),e.secure&&(i+="; Secure"),i+=`; SameSite=${e.sameSite[0].toUpperCase()}${e.sameSite.slice(1)}`,i}function x(t,n={}){return u(t,"",{...n,maxAge:0})}function p(){return(0,a.getRequestEvent)()?.request?.headers??null}function k(t){return s(p()?.get("cookie"))[t]??null}function C(){return s(p()?.get("cookie"))}
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/cookies.ts
21
+ var cookies_exports = {};
22
+ __export(cookies_exports, {
23
+ deleteCookie: () => deleteCookie,
24
+ getCookie: () => getCookie,
25
+ getCookies: () => getCookies,
26
+ parseCookies: () => parseCookies,
27
+ serializeCookie: () => serializeCookie
28
+ });
29
+ module.exports = __toCommonJS(cookies_exports);
30
+ var import_server = require("@fluixi/server");
31
+ function parseCookies(header) {
32
+ const out = {};
33
+ if (!header) return out;
34
+ for (const part of header.split(";")) {
35
+ const eq = part.indexOf("=");
36
+ if (eq < 0) continue;
37
+ const k = part.slice(0, eq).trim();
38
+ if (k) out[k] = decodeURIComponent(part.slice(eq + 1).trim());
39
+ }
40
+ return out;
41
+ }
42
+ function serializeCookie(name, value, opts = {}) {
43
+ const o = {
44
+ path: "/",
45
+ httpOnly: true,
46
+ secure: true,
47
+ sameSite: "lax",
48
+ ...opts
49
+ };
50
+ let s = `${name}=${encodeURIComponent(value)}`;
51
+ if (o.maxAge != null) s += `; Max-Age=${Math.floor(o.maxAge)}`;
52
+ if (o.expires) s += `; Expires=${o.expires.toUTCString()}`;
53
+ if (o.domain) s += `; Domain=${o.domain}`;
54
+ s += `; Path=${o.path}`;
55
+ if (o.httpOnly) s += "; HttpOnly";
56
+ if (o.secure) s += "; Secure";
57
+ s += `; SameSite=${o.sameSite[0].toUpperCase()}${o.sameSite.slice(1)}`;
58
+ return s;
59
+ }
60
+ function deleteCookie(name, opts = {}) {
61
+ return serializeCookie(name, "", { ...opts, maxAge: 0 });
62
+ }
63
+ function requestHeaders() {
64
+ const req = (0, import_server.getRequestEvent)()?.request;
65
+ return req?.headers ?? null;
66
+ }
67
+ function getCookie(name) {
68
+ return parseCookies(requestHeaders()?.get("cookie"))[name] ?? null;
69
+ }
70
+ function getCookies() {
71
+ return parseCookies(requestHeaders()?.get("cookie"));
72
+ }
package/dist/cookies.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Cookie API proper, secure-by-default cookie management for auth (and anything else).
2
+ * Cookie API: proper, secure-by-default cookie management for auth (and anything else).
3
3
  * Parsing + request-scoped reads work standalone; `serializeCookie` builds a spec-compliant
4
4
  * `Set-Cookie` string with safe defaults (HttpOnly, Secure, SameSite=Lax, Path=/) that
5
5
  * middleware appends to its Response (`response.headers.append('set-cookie', …)`).
package/dist/cookies.mjs CHANGED
@@ -1 +1,51 @@
1
- import{getRequestEvent as a}from"@fluixi/server";function r(n){let i={};if(!n)return i;for(let o of n.split(";")){let e=o.indexOf("=");if(e<0)continue;let t=o.slice(0,e).trim();t&&(i[t]=decodeURIComponent(o.slice(e+1).trim()))}return i}function u(n,i,o={}){let e={path:"/",httpOnly:!0,secure:!0,sameSite:"lax",...o},t=`${n}=${encodeURIComponent(i)}`;return e.maxAge!=null&&(t+=`; Max-Age=${Math.floor(e.maxAge)}`),e.expires&&(t+=`; Expires=${e.expires.toUTCString()}`),e.domain&&(t+=`; Domain=${e.domain}`),t+=`; Path=${e.path}`,e.httpOnly&&(t+="; HttpOnly"),e.secure&&(t+="; Secure"),t+=`; SameSite=${e.sameSite[0].toUpperCase()}${e.sameSite.slice(1)}`,t}function c(n,i={}){return u(n,"",{...i,maxAge:0})}function s(){return a()?.request?.headers??null}function l(n){return r(s()?.get("cookie"))[n]??null}function g(){return r(s()?.get("cookie"))}export{c as deleteCookie,l as getCookie,g as getCookies,r as parseCookies,u as serializeCookie};
1
+ // src/cookies.ts
2
+ import { getRequestEvent } from "@fluixi/server";
3
+ function parseCookies(header) {
4
+ const out = {};
5
+ if (!header) return out;
6
+ for (const part of header.split(";")) {
7
+ const eq = part.indexOf("=");
8
+ if (eq < 0) continue;
9
+ const k = part.slice(0, eq).trim();
10
+ if (k) out[k] = decodeURIComponent(part.slice(eq + 1).trim());
11
+ }
12
+ return out;
13
+ }
14
+ function serializeCookie(name, value, opts = {}) {
15
+ const o = {
16
+ path: "/",
17
+ httpOnly: true,
18
+ secure: true,
19
+ sameSite: "lax",
20
+ ...opts
21
+ };
22
+ let s = `${name}=${encodeURIComponent(value)}`;
23
+ if (o.maxAge != null) s += `; Max-Age=${Math.floor(o.maxAge)}`;
24
+ if (o.expires) s += `; Expires=${o.expires.toUTCString()}`;
25
+ if (o.domain) s += `; Domain=${o.domain}`;
26
+ s += `; Path=${o.path}`;
27
+ if (o.httpOnly) s += "; HttpOnly";
28
+ if (o.secure) s += "; Secure";
29
+ s += `; SameSite=${o.sameSite[0].toUpperCase()}${o.sameSite.slice(1)}`;
30
+ return s;
31
+ }
32
+ function deleteCookie(name, opts = {}) {
33
+ return serializeCookie(name, "", { ...opts, maxAge: 0 });
34
+ }
35
+ function requestHeaders() {
36
+ const req = getRequestEvent()?.request;
37
+ return req?.headers ?? null;
38
+ }
39
+ function getCookie(name) {
40
+ return parseCookies(requestHeaders()?.get("cookie"))[name] ?? null;
41
+ }
42
+ function getCookies() {
43
+ return parseCookies(requestHeaders()?.get("cookie"));
44
+ }
45
+ export {
46
+ deleteCookie,
47
+ getCookie,
48
+ getCookies,
49
+ parseCookies,
50
+ serializeCookie
51
+ };