@dashai/sdk 0.7.0

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -0
  3. package/dist/auth/client.cjs +185 -0
  4. package/dist/auth/client.cjs.map +1 -0
  5. package/dist/auth/client.d.cts +137 -0
  6. package/dist/auth/client.d.ts +137 -0
  7. package/dist/auth/client.js +175 -0
  8. package/dist/auth/client.js.map +1 -0
  9. package/dist/auth/middleware.cjs +352 -0
  10. package/dist/auth/middleware.cjs.map +1 -0
  11. package/dist/auth/middleware.d.cts +90 -0
  12. package/dist/auth/middleware.d.ts +90 -0
  13. package/dist/auth/middleware.js +343 -0
  14. package/dist/auth/middleware.js.map +1 -0
  15. package/dist/auth/server.cjs +445 -0
  16. package/dist/auth/server.cjs.map +1 -0
  17. package/dist/auth/server.d.cts +170 -0
  18. package/dist/auth/server.d.ts +170 -0
  19. package/dist/auth/server.js +432 -0
  20. package/dist/auth/server.js.map +1 -0
  21. package/dist/auth/types.cjs +31 -0
  22. package/dist/auth/types.cjs.map +1 -0
  23. package/dist/auth/types.d.cts +163 -0
  24. package/dist/auth/types.d.ts +163 -0
  25. package/dist/auth/types.js +29 -0
  26. package/dist/auth/types.js.map +1 -0
  27. package/dist/deps/index.cjs +117 -0
  28. package/dist/deps/index.cjs.map +1 -0
  29. package/dist/deps/index.d.cts +93 -0
  30. package/dist/deps/index.d.ts +93 -0
  31. package/dist/deps/index.js +112 -0
  32. package/dist/deps/index.js.map +1 -0
  33. package/dist/errors-BV75u7b9.d.cts +207 -0
  34. package/dist/errors-BV75u7b9.d.ts +207 -0
  35. package/dist/index.cjs +721 -0
  36. package/dist/index.cjs.map +1 -0
  37. package/dist/index.d.cts +171 -0
  38. package/dist/index.d.ts +171 -0
  39. package/dist/index.js +703 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/query/index.cjs +621 -0
  42. package/dist/query/index.cjs.map +1 -0
  43. package/dist/query/index.d.cts +800 -0
  44. package/dist/query/index.d.ts +800 -0
  45. package/dist/query/index.js +617 -0
  46. package/dist/query/index.js.map +1 -0
  47. package/dist/react/index.cjs +199 -0
  48. package/dist/react/index.cjs.map +1 -0
  49. package/dist/react/index.d.cts +117 -0
  50. package/dist/react/index.d.ts +117 -0
  51. package/dist/react/index.js +195 -0
  52. package/dist/react/index.js.map +1 -0
  53. package/dist/types-BLNQ1S1C.d.cts +396 -0
  54. package/dist/types-BLNQ1S1C.d.ts +396 -0
  55. package/package.json +109 -0
@@ -0,0 +1,137 @@
1
+ import { ReactNode, ReactElement } from 'react';
2
+ import { Session, SessionStatus } from './types.cjs';
3
+
4
+ /**
5
+ * Client-side React helpers — used from Client Components in Next.js
6
+ * App Router or in any React 18+ app that wants to read the current
7
+ * DashWise session.
8
+ *
9
+ * Consumers:
10
+ * import { AuthProvider, useSession, SignOutButton } from '@dashai/sdk/auth/client';
11
+ *
12
+ * Wire `<AuthProvider>` once at the root of your app (typically in
13
+ * `app/layout.tsx`); any descendant Client Component can then call
14
+ * `useSession()` to read the session reactively.
15
+ *
16
+ * Session fetching is lazy: the provider performs a single GET
17
+ * against `/api/auth/sessions/me` on mount. To force a re-fetch
18
+ * (e.g. after a `signOut()` + sign-in flow), call `refresh()` on the
19
+ * context.
20
+ */
21
+
22
+ interface SessionContextValue {
23
+ session: Session | null;
24
+ status: SessionStatus;
25
+ /** Force-refetch the session. Useful after a sign-in or sign-out. */
26
+ refresh: () => Promise<void>;
27
+ }
28
+ interface AuthProviderProps {
29
+ children: ReactNode;
30
+ /**
31
+ * Where to call `/api/auth/sessions/me`. Defaults to relative URL
32
+ * (same origin as the page) — that's the right default for custom
33
+ * Next.js modules where the backend is proxied through the same
34
+ * host. Pass an absolute URL to fetch from a cross-origin API.
35
+ */
36
+ apiBaseUrl?: string;
37
+ /**
38
+ * Optional initial session — useful for hydrating from a server
39
+ * render. When passed, the provider skips its initial fetch and
40
+ * starts in `authenticated` (or `unauthenticated` if null was
41
+ * explicitly passed and the consumer wants to opt out of the
42
+ * mount-time fetch).
43
+ */
44
+ initialSession?: Session | null;
45
+ }
46
+ /**
47
+ * React Context provider that loads the current session on mount and
48
+ * makes it available to descendant `useSession()` callers.
49
+ */
50
+ declare function AuthProvider({ children, apiBaseUrl, initialSession, }: AuthProviderProps): ReactElement;
51
+ /**
52
+ * Read the current session reactively. Returns `{ session, status,
53
+ * refresh }`. Always renders synchronously; the consumer should gate
54
+ * on `status === 'loading'` for the initial fetch.
55
+ *
56
+ * @example
57
+ * function Header() {
58
+ * const { session, status } = useSession();
59
+ * if (status === 'loading') return <Spinner />;
60
+ * if (!session) return <a href="/api/auth/sign-in">Sign in</a>;
61
+ * return <span>Hi {session.user.email}</span>;
62
+ * }
63
+ */
64
+ declare function useSession(): SessionContextValue;
65
+ interface SignOutButtonProps {
66
+ children?: ReactNode;
67
+ /**
68
+ * Where to redirect after sign-out. Defaults to `/`. Pass `null`
69
+ * to disable redirect (useful when the parent wants to handle
70
+ * navigation itself, e.g. inside a modal).
71
+ */
72
+ redirectTo?: string | null;
73
+ /** Optional className for styling. */
74
+ className?: string;
75
+ /** Apply to the underlying <button>. */
76
+ apiBaseUrl?: string;
77
+ }
78
+ /**
79
+ * Button that POSTs to `/api/auth/sign-out` and redirects on success.
80
+ * Pure plumbing — style/behavior tweaks via children + className.
81
+ *
82
+ * @example
83
+ * <SignOutButton className="text-sm text-gray-500">Log out</SignOutButton>
84
+ */
85
+ declare function SignOutButton({ children, redirectTo, className, apiBaseUrl, }: SignOutButtonProps): ReactElement;
86
+ interface SignInLinkProps {
87
+ children?: ReactNode;
88
+ className?: string;
89
+ /**
90
+ * Path to send the user to. Defaults to `/api/auth/sign-in`, which
91
+ * the SDK's route-handler counterpart redirects through the SSO
92
+ * flow. Pass an absolute URL to override.
93
+ */
94
+ href?: string;
95
+ /**
96
+ * Where to return after sign-in completes. Defaults to the current
97
+ * path (read from `window.location`). Pass an explicit string to
98
+ * route the user elsewhere.
99
+ */
100
+ returnTo?: string;
101
+ }
102
+ /**
103
+ * Link that initiates the sign-in flow. Preserves the current page
104
+ * as the post-sign-in destination via `?return_to=`.
105
+ */
106
+ declare function SignInLink({ children, className, href, returnTo, }: SignInLinkProps): ReactElement;
107
+ /**
108
+ * Does the current session hold `permissionKey`? Returns false during
109
+ * loading or when no session exists (default-deny floor).
110
+ *
111
+ * @example
112
+ * const canDelete = useHasPermission('tasks:delete');
113
+ * if (!canDelete) return null;
114
+ * return <button onClick={onDelete}>Delete</button>;
115
+ */
116
+ declare function useHasPermission(permissionKey: string): boolean;
117
+ /**
118
+ * Same as `useHasPermission` but for roles.
119
+ */
120
+ declare function useHasRole(roleKey: string): boolean;
121
+ /**
122
+ * Return the full permission list. Useful for complex conditional
123
+ * rendering across many checkboxes / toolbar items. Returns an empty
124
+ * array during loading.
125
+ */
126
+ declare function usePermissions(): string[];
127
+ /**
128
+ * Return the full role list. Useful for showing/hiding sections
129
+ * based on coarse role membership.
130
+ */
131
+ declare function useRoles(): string[];
132
+ /**
133
+ * Convenience: check if ANY of the given permissions is held.
134
+ */
135
+ declare function useHasAnyPermission(permissionKeys: readonly string[]): boolean;
136
+
137
+ export { AuthProvider, type AuthProviderProps, type SessionContextValue, SignInLink, type SignInLinkProps, SignOutButton, type SignOutButtonProps, useHasAnyPermission, useHasPermission, useHasRole, usePermissions, useRoles, useSession };
@@ -0,0 +1,137 @@
1
+ import { ReactNode, ReactElement } from 'react';
2
+ import { Session, SessionStatus } from './types.js';
3
+
4
+ /**
5
+ * Client-side React helpers — used from Client Components in Next.js
6
+ * App Router or in any React 18+ app that wants to read the current
7
+ * DashWise session.
8
+ *
9
+ * Consumers:
10
+ * import { AuthProvider, useSession, SignOutButton } from '@dashai/sdk/auth/client';
11
+ *
12
+ * Wire `<AuthProvider>` once at the root of your app (typically in
13
+ * `app/layout.tsx`); any descendant Client Component can then call
14
+ * `useSession()` to read the session reactively.
15
+ *
16
+ * Session fetching is lazy: the provider performs a single GET
17
+ * against `/api/auth/sessions/me` on mount. To force a re-fetch
18
+ * (e.g. after a `signOut()` + sign-in flow), call `refresh()` on the
19
+ * context.
20
+ */
21
+
22
+ interface SessionContextValue {
23
+ session: Session | null;
24
+ status: SessionStatus;
25
+ /** Force-refetch the session. Useful after a sign-in or sign-out. */
26
+ refresh: () => Promise<void>;
27
+ }
28
+ interface AuthProviderProps {
29
+ children: ReactNode;
30
+ /**
31
+ * Where to call `/api/auth/sessions/me`. Defaults to relative URL
32
+ * (same origin as the page) — that's the right default for custom
33
+ * Next.js modules where the backend is proxied through the same
34
+ * host. Pass an absolute URL to fetch from a cross-origin API.
35
+ */
36
+ apiBaseUrl?: string;
37
+ /**
38
+ * Optional initial session — useful for hydrating from a server
39
+ * render. When passed, the provider skips its initial fetch and
40
+ * starts in `authenticated` (or `unauthenticated` if null was
41
+ * explicitly passed and the consumer wants to opt out of the
42
+ * mount-time fetch).
43
+ */
44
+ initialSession?: Session | null;
45
+ }
46
+ /**
47
+ * React Context provider that loads the current session on mount and
48
+ * makes it available to descendant `useSession()` callers.
49
+ */
50
+ declare function AuthProvider({ children, apiBaseUrl, initialSession, }: AuthProviderProps): ReactElement;
51
+ /**
52
+ * Read the current session reactively. Returns `{ session, status,
53
+ * refresh }`. Always renders synchronously; the consumer should gate
54
+ * on `status === 'loading'` for the initial fetch.
55
+ *
56
+ * @example
57
+ * function Header() {
58
+ * const { session, status } = useSession();
59
+ * if (status === 'loading') return <Spinner />;
60
+ * if (!session) return <a href="/api/auth/sign-in">Sign in</a>;
61
+ * return <span>Hi {session.user.email}</span>;
62
+ * }
63
+ */
64
+ declare function useSession(): SessionContextValue;
65
+ interface SignOutButtonProps {
66
+ children?: ReactNode;
67
+ /**
68
+ * Where to redirect after sign-out. Defaults to `/`. Pass `null`
69
+ * to disable redirect (useful when the parent wants to handle
70
+ * navigation itself, e.g. inside a modal).
71
+ */
72
+ redirectTo?: string | null;
73
+ /** Optional className for styling. */
74
+ className?: string;
75
+ /** Apply to the underlying <button>. */
76
+ apiBaseUrl?: string;
77
+ }
78
+ /**
79
+ * Button that POSTs to `/api/auth/sign-out` and redirects on success.
80
+ * Pure plumbing — style/behavior tweaks via children + className.
81
+ *
82
+ * @example
83
+ * <SignOutButton className="text-sm text-gray-500">Log out</SignOutButton>
84
+ */
85
+ declare function SignOutButton({ children, redirectTo, className, apiBaseUrl, }: SignOutButtonProps): ReactElement;
86
+ interface SignInLinkProps {
87
+ children?: ReactNode;
88
+ className?: string;
89
+ /**
90
+ * Path to send the user to. Defaults to `/api/auth/sign-in`, which
91
+ * the SDK's route-handler counterpart redirects through the SSO
92
+ * flow. Pass an absolute URL to override.
93
+ */
94
+ href?: string;
95
+ /**
96
+ * Where to return after sign-in completes. Defaults to the current
97
+ * path (read from `window.location`). Pass an explicit string to
98
+ * route the user elsewhere.
99
+ */
100
+ returnTo?: string;
101
+ }
102
+ /**
103
+ * Link that initiates the sign-in flow. Preserves the current page
104
+ * as the post-sign-in destination via `?return_to=`.
105
+ */
106
+ declare function SignInLink({ children, className, href, returnTo, }: SignInLinkProps): ReactElement;
107
+ /**
108
+ * Does the current session hold `permissionKey`? Returns false during
109
+ * loading or when no session exists (default-deny floor).
110
+ *
111
+ * @example
112
+ * const canDelete = useHasPermission('tasks:delete');
113
+ * if (!canDelete) return null;
114
+ * return <button onClick={onDelete}>Delete</button>;
115
+ */
116
+ declare function useHasPermission(permissionKey: string): boolean;
117
+ /**
118
+ * Same as `useHasPermission` but for roles.
119
+ */
120
+ declare function useHasRole(roleKey: string): boolean;
121
+ /**
122
+ * Return the full permission list. Useful for complex conditional
123
+ * rendering across many checkboxes / toolbar items. Returns an empty
124
+ * array during loading.
125
+ */
126
+ declare function usePermissions(): string[];
127
+ /**
128
+ * Return the full role list. Useful for showing/hiding sections
129
+ * based on coarse role membership.
130
+ */
131
+ declare function useRoles(): string[];
132
+ /**
133
+ * Convenience: check if ANY of the given permissions is held.
134
+ */
135
+ declare function useHasAnyPermission(permissionKeys: readonly string[]): boolean;
136
+
137
+ export { AuthProvider, type AuthProviderProps, type SessionContextValue, SignInLink, type SignInLinkProps, SignOutButton, type SignOutButtonProps, useHasAnyPermission, useHasPermission, useHasRole, usePermissions, useRoles, useSession };
@@ -0,0 +1,175 @@
1
+ 'use client';
2
+ import { createContext, useState, useCallback, useEffect, useMemo, useContext } from 'react';
3
+ import { jsx } from 'react/jsx-runtime';
4
+
5
+ // src/auth/rbac.ts
6
+ function permissionGranted(rbac, key) {
7
+ if (!rbac || !rbac.permissions) return false;
8
+ return matchKey(rbac.permissions, key);
9
+ }
10
+ function roleGranted(rbac, roleKey) {
11
+ if (!rbac || !rbac.roles) return false;
12
+ return matchKey(rbac.roles, roleKey);
13
+ }
14
+ function rbacOf(session) {
15
+ if (!session) return null;
16
+ return {
17
+ roles: session.rbacRoles ?? [],
18
+ permissions: session.rbacPermissions ?? []
19
+ };
20
+ }
21
+ function matchKey(stored, queryKey) {
22
+ if (stored.length === 0) return false;
23
+ for (const s of stored) {
24
+ if (s === queryKey) return true;
25
+ if (s.endsWith(":" + queryKey)) return true;
26
+ }
27
+ return false;
28
+ }
29
+ var SessionContext = createContext({
30
+ session: null,
31
+ status: "loading",
32
+ refresh: async () => {
33
+ }
34
+ });
35
+ function AuthProvider({
36
+ children,
37
+ apiBaseUrl,
38
+ initialSession
39
+ }) {
40
+ const [session, setSession] = useState(initialSession ?? null);
41
+ const [status, setStatus] = useState(
42
+ initialSession === void 0 ? "loading" : initialSession ? "authenticated" : "unauthenticated"
43
+ );
44
+ const refresh = useCallback(async () => {
45
+ setStatus("loading");
46
+ try {
47
+ const base = apiBaseUrl ?? "";
48
+ const res = await fetch(`${base}/api/auth/sessions/me`, {
49
+ method: "GET",
50
+ credentials: "include",
51
+ headers: { Accept: "application/json" }
52
+ });
53
+ if (!res.ok) {
54
+ setSession(null);
55
+ setStatus("unauthenticated");
56
+ return;
57
+ }
58
+ const data = await res.json().catch(() => null);
59
+ if (!data) {
60
+ setSession(null);
61
+ setStatus("unauthenticated");
62
+ return;
63
+ }
64
+ let enriched = data;
65
+ if (typeof data.installationId === "number") {
66
+ const rbacRes = await fetch(
67
+ `${base}/api/installations/${data.installationId}/rbac/me`,
68
+ {
69
+ method: "GET",
70
+ credentials: "include",
71
+ headers: { Accept: "application/json" }
72
+ }
73
+ ).catch(() => null);
74
+ if (rbacRes && rbacRes.ok) {
75
+ const rbac = await rbacRes.json().catch(() => null);
76
+ if (rbac && Array.isArray(rbac.roles) && Array.isArray(rbac.permissions)) {
77
+ enriched = {
78
+ ...data,
79
+ rbacRoles: rbac.roles.filter(
80
+ (r) => typeof r === "string"
81
+ ),
82
+ rbacPermissions: rbac.permissions.filter(
83
+ (p) => typeof p === "string"
84
+ )
85
+ };
86
+ }
87
+ }
88
+ }
89
+ setSession(enriched);
90
+ setStatus("authenticated");
91
+ } catch {
92
+ setSession(null);
93
+ setStatus("unauthenticated");
94
+ }
95
+ }, [apiBaseUrl]);
96
+ useEffect(() => {
97
+ if (initialSession !== void 0) return;
98
+ void refresh();
99
+ }, []);
100
+ const value = useMemo(
101
+ () => ({ session, status, refresh }),
102
+ [session, status, refresh]
103
+ );
104
+ return /* @__PURE__ */ jsx(SessionContext.Provider, { value, children });
105
+ }
106
+ function useSession() {
107
+ return useContext(SessionContext);
108
+ }
109
+ function SignOutButton({
110
+ children = "Sign out",
111
+ redirectTo = "/",
112
+ className,
113
+ apiBaseUrl
114
+ }) {
115
+ const { refresh } = useSession();
116
+ const onClick = useCallback(async () => {
117
+ try {
118
+ const base = apiBaseUrl ?? "";
119
+ await fetch(`${base}/api/auth/sign-out`, {
120
+ method: "POST",
121
+ credentials: "include"
122
+ });
123
+ } catch {
124
+ }
125
+ await refresh();
126
+ if (redirectTo !== null && typeof window !== "undefined") {
127
+ window.location.href = redirectTo;
128
+ }
129
+ }, [apiBaseUrl, redirectTo, refresh]);
130
+ return /* @__PURE__ */ jsx("button", { type: "button", onClick, className, children });
131
+ }
132
+ function SignInLink({
133
+ children = "Sign in",
134
+ className,
135
+ href = "/api/auth/sign-in",
136
+ returnTo
137
+ }) {
138
+ const target = useMemo(() => {
139
+ const computedReturnTo = returnTo ?? (typeof window !== "undefined" ? window.location.pathname + window.location.search : "/");
140
+ const url = new URL(href, typeof window !== "undefined" ? window.location.origin : "http://x");
141
+ url.searchParams.set("return_to", computedReturnTo);
142
+ return url.pathname + url.search;
143
+ }, [href, returnTo]);
144
+ return /* @__PURE__ */ jsx("a", { href: target, className, children });
145
+ }
146
+ function useHasPermission(permissionKey) {
147
+ const { session } = useSession();
148
+ return permissionGranted(rbacOf(session), permissionKey);
149
+ }
150
+ function useHasRole(roleKey) {
151
+ const { session } = useSession();
152
+ return roleGranted(rbacOf(session), roleKey);
153
+ }
154
+ function usePermissions() {
155
+ const { session } = useSession();
156
+ return session?.rbacPermissions ?? [];
157
+ }
158
+ function useRoles() {
159
+ const { session } = useSession();
160
+ return session?.rbacRoles ?? [];
161
+ }
162
+ function useHasAnyPermission(permissionKeys) {
163
+ const perms = usePermissions();
164
+ if (perms.length === 0) return false;
165
+ for (const key of permissionKeys) {
166
+ for (const p of perms) {
167
+ if (p === key || p.endsWith(":" + key)) return true;
168
+ }
169
+ }
170
+ return false;
171
+ }
172
+
173
+ export { AuthProvider, SignInLink, SignOutButton, useHasAnyPermission, useHasPermission, useHasRole, usePermissions, useRoles, useSession };
174
+ //# sourceMappingURL=client.js.map
175
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/rbac.ts","../../src/auth/client.tsx"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["/**\n * Module RBAC helpers — shared between server-side (`auth/server.ts`),\n * middleware (`auth/middleware.ts`), and client-side (`auth/client.tsx`).\n *\n * The design lives at\n * `dashwise-devops-docs/plans/module-rbac-design.md` (Locked\n * 2026-05-17). This file implements the **runtime-side resolution**\n * — the actual check against a resolved permission/role set.\n *\n * **Where the resolved set comes from** depends on the caller:\n * - Server-side: `getServerSession()` fetches `/api/installations/:id/rbac/me`\n * in parallel with `/api/auth/sessions/me` and merges into the\n * returned `Session`.\n * - Client-side: `AuthProvider` does the same on mount, exposes via\n * React context.\n *\n * **Bare vs namespaced keys.** Internally we store namespaced keys\n * (`<module_slug>:<key>`) so the JWT shape supports multi-install in\n * the future. Authors write BARE keys (`tasks:delete`, `editor`) from\n * inside their own module — the helpers transparently match either.\n *\n * The bare-key match is `endsWith(':' + bareKey)` — safe because role\n * keys can't contain colons (enforced by the parser regex in P12.1)\n * and permission keys use colons as namespace separators within a\n * fixed shape. Cross-module references must use the full namespaced\n * form explicitly.\n */\n\nimport type { RbacMeResponse, Session } from './types.js';\n\n/**\n * Does the caller's resolved permission set contain `key`? Accepts\n * either the bare form (`tasks:delete`) or the fully-namespaced form\n * (`tasks-app:tasks:delete`).\n *\n * Pure function over a `RbacMeResponse`-shaped object — usable from\n * any context. Returns `false` if the set is missing/undefined (the\n * default-deny floor); never throws.\n */\nexport function permissionGranted(\n rbac: Pick<RbacMeResponse, 'permissions'> | null | undefined,\n key: string,\n): boolean {\n if (!rbac || !rbac.permissions) return false;\n return matchKey(rbac.permissions, key);\n}\n\n/**\n * Does the caller hold `roleKey` (bare or namespaced) in their role\n * set? Returns false if no RBAC data present.\n */\nexport function roleGranted(\n rbac: Pick<RbacMeResponse, 'roles'> | null | undefined,\n roleKey: string,\n): boolean {\n if (!rbac || !rbac.roles) return false;\n return matchKey(rbac.roles, roleKey);\n}\n\n/**\n * Does the resolved set contain ANY of the requested permissions?\n * Used by `withAnyPermission()` middleware + the `any: [...]` rule\n * shape in `withAuth.permissionRules`.\n */\nexport function anyPermissionGranted(\n rbac: Pick<RbacMeResponse, 'permissions'> | null | undefined,\n keys: readonly string[],\n): boolean {\n return keys.some((k) => permissionGranted(rbac, k));\n}\n\n/**\n * Does the resolved set contain ALL of the requested permissions?\n * Used by `withAllPermissions()` middleware.\n */\nexport function allPermissionsGranted(\n rbac: Pick<RbacMeResponse, 'permissions'> | null | undefined,\n keys: readonly string[],\n): boolean {\n return keys.every((k) => permissionGranted(rbac, k));\n}\n\n/**\n * Extract just the RBAC slice from a `Session`. Tiny helper for\n * helpers that want to pass either a Session or a `RbacMeResponse`\n * shape uniformly.\n */\nexport function rbacOf(session: Session | null | undefined):\n | Pick<RbacMeResponse, 'roles' | 'permissions'>\n | null {\n if (!session) return null;\n return {\n roles: session.rbacRoles ?? [],\n permissions: session.rbacPermissions ?? [],\n };\n}\n\n// ── Private match helper ───────────────────────────────────────────\n\n/**\n * Bare-or-namespaced match against a stored key set.\n *\n * exact match → granted\n * key ends with `:bare` → granted (namespaced match)\n *\n * The `:` prefix on the suffix check prevents `'admin'` from\n * matching a stored `'super-admin'`. Role + permission keys never\n * start with `:` and never contain it un-namespaced, so this match\n * is safe across both kinds.\n */\nfunction matchKey(stored: readonly string[], queryKey: string): boolean {\n if (stored.length === 0) return false;\n for (const s of stored) {\n if (s === queryKey) return true;\n if (s.endsWith(':' + queryKey)) return true;\n }\n return false;\n}\n","'use client';\n\n/**\n * Client-side React helpers — used from Client Components in Next.js\n * App Router or in any React 18+ app that wants to read the current\n * DashWise session.\n *\n * Consumers:\n * import { AuthProvider, useSession, SignOutButton } from '@dashai/sdk/auth/client';\n *\n * Wire `<AuthProvider>` once at the root of your app (typically in\n * `app/layout.tsx`); any descendant Client Component can then call\n * `useSession()` to read the session reactively.\n *\n * Session fetching is lazy: the provider performs a single GET\n * against `/api/auth/sessions/me` on mount. To force a re-fetch\n * (e.g. after a `signOut()` + sign-in flow), call `refresh()` on the\n * context.\n */\n\nimport {\n createContext,\n type ReactElement,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from 'react';\n\nimport type { RbacMeResponse, Session, SessionStatus } from './types.js';\nimport {\n permissionGranted,\n rbacOf,\n roleGranted,\n} from './rbac.js';\n\n// ── Context ────────────────────────────────────────────────────────────\n\nexport interface SessionContextValue {\n session: Session | null;\n status: SessionStatus;\n /** Force-refetch the session. Useful after a sign-in or sign-out. */\n refresh: () => Promise<void>;\n}\n\nconst SessionContext = createContext<SessionContextValue>({\n session: null,\n status: 'loading',\n refresh: async () => {\n // No-op outside a provider. Consumers should always render\n // `<AuthProvider>` at the root.\n },\n});\n\n// ── Provider ──────────────────────────────────────────────────────────\n\nexport interface AuthProviderProps {\n children: ReactNode;\n /**\n * Where to call `/api/auth/sessions/me`. Defaults to relative URL\n * (same origin as the page) — that's the right default for custom\n * Next.js modules where the backend is proxied through the same\n * host. Pass an absolute URL to fetch from a cross-origin API.\n */\n apiBaseUrl?: string;\n /**\n * Optional initial session — useful for hydrating from a server\n * render. When passed, the provider skips its initial fetch and\n * starts in `authenticated` (or `unauthenticated` if null was\n * explicitly passed and the consumer wants to opt out of the\n * mount-time fetch).\n */\n initialSession?: Session | null;\n}\n\n/**\n * React Context provider that loads the current session on mount and\n * makes it available to descendant `useSession()` callers.\n */\nexport function AuthProvider({\n children,\n apiBaseUrl,\n initialSession,\n}: AuthProviderProps): ReactElement {\n const [session, setSession] = useState<Session | null>(initialSession ?? null);\n const [status, setStatus] = useState<SessionStatus>(\n initialSession === undefined ? 'loading' : initialSession ? 'authenticated' : 'unauthenticated',\n );\n\n const refresh = useCallback(async () => {\n setStatus('loading');\n try {\n const base = apiBaseUrl ?? '';\n const res = await fetch(`${base}/api/auth/sessions/me`, {\n method: 'GET',\n credentials: 'include',\n headers: { Accept: 'application/json' },\n });\n if (!res.ok) {\n setSession(null);\n setStatus('unauthenticated');\n return;\n }\n const data = (await res.json().catch(() => null)) as Session | null;\n if (!data) {\n setSession(null);\n setStatus('unauthenticated');\n return;\n }\n\n // P12.4: fetch the RBAC slice for the current installation in\n // parallel. Failure is non-fatal — the session still renders;\n // `useHasPermission` will simply return false for everything\n // until a successful re-fetch (the default-deny floor).\n let enriched: Session = data;\n if (typeof data.installationId === 'number') {\n const rbacRes = await fetch(\n `${base}/api/installations/${data.installationId}/rbac/me`,\n {\n method: 'GET',\n credentials: 'include',\n headers: { Accept: 'application/json' },\n },\n ).catch(() => null);\n\n if (rbacRes && rbacRes.ok) {\n const rbac = (await rbacRes.json().catch(() => null)) as\n | RbacMeResponse\n | null;\n if (\n rbac &&\n Array.isArray(rbac.roles) &&\n Array.isArray(rbac.permissions)\n ) {\n enriched = {\n ...data,\n rbacRoles: rbac.roles.filter(\n (r: unknown): r is string => typeof r === 'string',\n ),\n rbacPermissions: rbac.permissions.filter(\n (p: unknown): p is string => typeof p === 'string',\n ),\n };\n }\n }\n }\n setSession(enriched);\n setStatus('authenticated');\n } catch {\n setSession(null);\n setStatus('unauthenticated');\n }\n }, [apiBaseUrl]);\n\n useEffect(() => {\n if (initialSession !== undefined) return; // skip mount-time fetch\n void refresh();\n // We deliberately don't depend on `refresh` directly — the\n // useCallback handles staleness; we only want to fire on mount.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const value = useMemo<SessionContextValue>(\n () => ({ session, status, refresh }),\n [session, status, refresh],\n );\n\n return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;\n}\n\n// ── Hook ──────────────────────────────────────────────────────────────\n\n/**\n * Read the current session reactively. Returns `{ session, status,\n * refresh }`. Always renders synchronously; the consumer should gate\n * on `status === 'loading'` for the initial fetch.\n *\n * @example\n * function Header() {\n * const { session, status } = useSession();\n * if (status === 'loading') return <Spinner />;\n * if (!session) return <a href=\"/api/auth/sign-in\">Sign in</a>;\n * return <span>Hi {session.user.email}</span>;\n * }\n */\nexport function useSession(): SessionContextValue {\n return useContext(SessionContext);\n}\n\n// ── Components ────────────────────────────────────────────────────────\n\nexport interface SignOutButtonProps {\n children?: ReactNode;\n /**\n * Where to redirect after sign-out. Defaults to `/`. Pass `null`\n * to disable redirect (useful when the parent wants to handle\n * navigation itself, e.g. inside a modal).\n */\n redirectTo?: string | null;\n /** Optional className for styling. */\n className?: string;\n /** Apply to the underlying <button>. */\n apiBaseUrl?: string;\n}\n\n/**\n * Button that POSTs to `/api/auth/sign-out` and redirects on success.\n * Pure plumbing — style/behavior tweaks via children + className.\n *\n * @example\n * <SignOutButton className=\"text-sm text-gray-500\">Log out</SignOutButton>\n */\nexport function SignOutButton({\n children = 'Sign out',\n redirectTo = '/',\n className,\n apiBaseUrl,\n}: SignOutButtonProps): ReactElement {\n const { refresh } = useSession();\n const onClick = useCallback(async () => {\n try {\n const base = apiBaseUrl ?? '';\n await fetch(`${base}/api/auth/sign-out`, {\n method: 'POST',\n credentials: 'include',\n });\n } catch {\n // Best-effort. Even if the request fails, clear local session\n // state below so the UI reflects \"signed out\".\n }\n await refresh();\n if (redirectTo !== null && typeof window !== 'undefined') {\n window.location.href = redirectTo;\n }\n }, [apiBaseUrl, redirectTo, refresh]);\n\n return (\n <button type=\"button\" onClick={onClick} className={className}>\n {children}\n </button>\n );\n}\n\nexport interface SignInLinkProps {\n children?: ReactNode;\n className?: string;\n /**\n * Path to send the user to. Defaults to `/api/auth/sign-in`, which\n * the SDK's route-handler counterpart redirects through the SSO\n * flow. Pass an absolute URL to override.\n */\n href?: string;\n /**\n * Where to return after sign-in completes. Defaults to the current\n * path (read from `window.location`). Pass an explicit string to\n * route the user elsewhere.\n */\n returnTo?: string;\n}\n\n/**\n * Link that initiates the sign-in flow. Preserves the current page\n * as the post-sign-in destination via `?return_to=`.\n */\nexport function SignInLink({\n children = 'Sign in',\n className,\n href = '/api/auth/sign-in',\n returnTo,\n}: SignInLinkProps): ReactElement {\n const target = useMemo(() => {\n const computedReturnTo =\n returnTo ??\n (typeof window !== 'undefined' ? window.location.pathname + window.location.search : '/');\n const url = new URL(href, typeof window !== 'undefined' ? window.location.origin : 'http://x');\n url.searchParams.set('return_to', computedReturnTo);\n // Return relative path for in-app navigation when possible.\n return url.pathname + url.search;\n }, [href, returnTo]);\n\n return (\n <a href={target} className={className}>\n {children}\n </a>\n );\n}\n\n// ── Module RBAC hooks (P12.4) ─────────────────────────────────────────\n//\n// Reads the resolved RBAC slice (`rbacRoles` / `rbacPermissions`) from\n// the session populated by `AuthProvider`. All hooks are sync after\n// the initial session fetch — no network calls per check.\n//\n// Accept either the BARE form (`tasks:delete`, `editor`) or the\n// NAMESPACED form (`tasks-app:tasks:delete`). Authors writing inside\n// their own module should use the bare form for ergonomics.\n\n/**\n * Does the current session hold `permissionKey`? Returns false during\n * loading or when no session exists (default-deny floor).\n *\n * @example\n * const canDelete = useHasPermission('tasks:delete');\n * if (!canDelete) return null;\n * return <button onClick={onDelete}>Delete</button>;\n */\nexport function useHasPermission(permissionKey: string): boolean {\n const { session } = useSession();\n return permissionGranted(rbacOf(session), permissionKey);\n}\n\n/**\n * Same as `useHasPermission` but for roles.\n */\nexport function useHasRole(roleKey: string): boolean {\n const { session } = useSession();\n return roleGranted(rbacOf(session), roleKey);\n}\n\n/**\n * Return the full permission list. Useful for complex conditional\n * rendering across many checkboxes / toolbar items. Returns an empty\n * array during loading.\n */\nexport function usePermissions(): string[] {\n const { session } = useSession();\n return session?.rbacPermissions ?? [];\n}\n\n/**\n * Return the full role list. Useful for showing/hiding sections\n * based on coarse role membership.\n */\nexport function useRoles(): string[] {\n const { session } = useSession();\n return session?.rbacRoles ?? [];\n}\n\n/**\n * Convenience: check if ANY of the given permissions is held.\n */\nexport function useHasAnyPermission(permissionKeys: readonly string[]): boolean {\n const perms = usePermissions();\n if (perms.length === 0) return false;\n for (const key of permissionKeys) {\n for (const p of perms) {\n if (p === key || p.endsWith(':' + key)) return true;\n }\n }\n return false;\n}\n"]}