@orangecheck/auth-client 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 OrangeCheck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # `@orangecheck/auth-client`
2
+
3
+ **React bindings for the cross-subdomain oc_session.**
4
+
5
+ Drop a provider near your app root, then consume the signed-in identity
6
+ from any subdomain in the `.ochk.io` ecosystem via hooks or a prebuilt
7
+ sign-in button.
8
+
9
+ Pairs with [`@orangecheck/auth-core`](../auth-core) on the server side.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ yarn add @orangecheck/auth-client @orangecheck/auth-core
15
+ ```
16
+
17
+ ## Mount the provider
18
+
19
+ ```tsx
20
+ // app root (e.g. pages/_app.tsx)
21
+ import { OcSessionProvider } from '@orangecheck/auth-client';
22
+
23
+ export default function App({ Component, pageProps }) {
24
+ return (
25
+ <OcSessionProvider
26
+ config={{
27
+ authOrigin: 'https://ochk.io', // the subdomain that owns sign-in
28
+ }}
29
+ >
30
+ <Component {...pageProps} />
31
+ </OcSessionProvider>
32
+ );
33
+ }
34
+ ```
35
+
36
+ The provider hits `GET /api/auth/me` (same origin) to load the current
37
+ session. Your app is responsible for implementing that endpoint using
38
+ `@orangecheck/auth-core`'s `verifySessionToken()`.
39
+
40
+ ## Read the session
41
+
42
+ ```tsx
43
+ import { useOcSession } from '@orangecheck/auth-client';
44
+
45
+ function Header() {
46
+ const { status, account, signOut } = useOcSession();
47
+
48
+ if (status === 'loading') return null;
49
+ if (status === 'anonymous') return <a href="/signin">sign in</a>;
50
+
51
+ return (
52
+ <>
53
+ <span>{account!.address}</span>
54
+ <button onClick={() => signOut()}>sign out</button>
55
+ </>
56
+ );
57
+ }
58
+ ```
59
+
60
+ ## Drop-in components
61
+
62
+ ```tsx
63
+ import { OcSignInButton, OcAccountPill } from '@orangecheck/auth-client';
64
+
65
+ // Shows a sign-in link when anonymous, nothing when signed in.
66
+ <OcSignInButton className="font-mono text-xs uppercase" />
67
+
68
+ // Shows the signed-in address pill; nothing when anonymous.
69
+ <OcAccountPill dashboardUrl="https://attest.ochk.io/dashboard" />
70
+ ```
71
+
72
+ Both components use CSS classes from the caller — no enforced styling,
73
+ so they work with Tailwind, vanilla CSS, or whatever.
74
+
75
+ ## API
76
+
77
+ ```ts
78
+ OcSessionProvider({ children, config?, defaultReturnTo? })
79
+
80
+ useOcSession(): {
81
+ status: 'loading' | 'authenticated' | 'anonymous' | 'error';
82
+ account: OcAccount | null;
83
+ error: Error | null;
84
+ refresh(): Promise<void>;
85
+ signOut(): Promise<void>;
86
+ signInUrl: string;
87
+ }
88
+
89
+ useOptionalOcSession(): OcSessionState | null // no-throw variant
90
+
91
+ <OcSignInButton label? eager? ...anchorProps />
92
+ <OcAccountPill dashboardUrl? render? ...divProps />
93
+ ```
94
+
95
+ ## License
96
+
97
+ MIT.
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+
3
+ interface OcAccount {
4
+ accountId: string;
5
+ address: string;
6
+ displayName?: string | null;
7
+ nostrNpub?: string | null;
8
+ }
9
+ type OcSessionStatus = 'loading' | 'authenticated' | 'anonymous' | 'error';
10
+ interface OcSessionState {
11
+ status: OcSessionStatus;
12
+ account: OcAccount | null;
13
+ error: Error | null;
14
+ refresh: () => Promise<void>;
15
+ signOut: () => Promise<void>;
16
+ signInUrl: string;
17
+ }
18
+ interface OcAuthConfig {
19
+ authOrigin?: string;
20
+ signInPath?: string;
21
+ mePath?: string;
22
+ logoutPath?: string;
23
+ }
24
+ declare const DEFAULT_CONFIG: Required<OcAuthConfig>;
25
+ declare function buildSignInUrl(cfg: Required<OcAuthConfig>, returnTo?: string): string;
26
+
27
+ interface OcSessionProviderProps {
28
+ children: React.ReactNode;
29
+ config?: OcAuthConfig;
30
+ defaultReturnTo?: string;
31
+ }
32
+ declare function OcSessionProvider({ children, config, defaultReturnTo, }: OcSessionProviderProps): React.ReactElement;
33
+ declare function useOcSession(): OcSessionState;
34
+ declare function useOptionalOcSession(): OcSessionState | null;
35
+
36
+ interface OcSignInButtonProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
37
+ label?: string;
38
+ eager?: boolean;
39
+ }
40
+ declare function OcSignInButton({ label, eager, className, ...rest }: OcSignInButtonProps): React.ReactElement | null;
41
+ interface OcAccountPillProps extends React.HTMLAttributes<HTMLDivElement> {
42
+ dashboardUrl?: string;
43
+ render?: (account: {
44
+ address: string;
45
+ displayName?: string | null;
46
+ }) => React.ReactNode;
47
+ }
48
+ declare function OcAccountPill({ dashboardUrl, render, className, ...rest }: OcAccountPillProps): React.ReactElement | null;
49
+
50
+ export { DEFAULT_CONFIG, type OcAccount, OcAccountPill, type OcAccountPillProps, type OcAuthConfig, OcSessionProvider, type OcSessionState, type OcSessionStatus, OcSignInButton, type OcSignInButtonProps, buildSignInUrl, useOcSession, useOptionalOcSession };
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+
3
+ interface OcAccount {
4
+ accountId: string;
5
+ address: string;
6
+ displayName?: string | null;
7
+ nostrNpub?: string | null;
8
+ }
9
+ type OcSessionStatus = 'loading' | 'authenticated' | 'anonymous' | 'error';
10
+ interface OcSessionState {
11
+ status: OcSessionStatus;
12
+ account: OcAccount | null;
13
+ error: Error | null;
14
+ refresh: () => Promise<void>;
15
+ signOut: () => Promise<void>;
16
+ signInUrl: string;
17
+ }
18
+ interface OcAuthConfig {
19
+ authOrigin?: string;
20
+ signInPath?: string;
21
+ mePath?: string;
22
+ logoutPath?: string;
23
+ }
24
+ declare const DEFAULT_CONFIG: Required<OcAuthConfig>;
25
+ declare function buildSignInUrl(cfg: Required<OcAuthConfig>, returnTo?: string): string;
26
+
27
+ interface OcSessionProviderProps {
28
+ children: React.ReactNode;
29
+ config?: OcAuthConfig;
30
+ defaultReturnTo?: string;
31
+ }
32
+ declare function OcSessionProvider({ children, config, defaultReturnTo, }: OcSessionProviderProps): React.ReactElement;
33
+ declare function useOcSession(): OcSessionState;
34
+ declare function useOptionalOcSession(): OcSessionState | null;
35
+
36
+ interface OcSignInButtonProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
37
+ label?: string;
38
+ eager?: boolean;
39
+ }
40
+ declare function OcSignInButton({ label, eager, className, ...rest }: OcSignInButtonProps): React.ReactElement | null;
41
+ interface OcAccountPillProps extends React.HTMLAttributes<HTMLDivElement> {
42
+ dashboardUrl?: string;
43
+ render?: (account: {
44
+ address: string;
45
+ displayName?: string | null;
46
+ }) => React.ReactNode;
47
+ }
48
+ declare function OcAccountPill({ dashboardUrl, render, className, ...rest }: OcAccountPillProps): React.ReactElement | null;
49
+
50
+ export { DEFAULT_CONFIG, type OcAccount, OcAccountPill, type OcAccountPillProps, type OcAuthConfig, OcSessionProvider, type OcSessionState, type OcSessionStatus, OcSignInButton, type OcSignInButtonProps, buildSignInUrl, useOcSession, useOptionalOcSession };
package/dist/index.js ADDED
@@ -0,0 +1,209 @@
1
+ 'use strict';
2
+
3
+ var React = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ function _interopNamespace(e) {
7
+ if (e && e.__esModule) return e;
8
+ var n = Object.create(null);
9
+ if (e) {
10
+ Object.keys(e).forEach(function (k) {
11
+ if (k !== 'default') {
12
+ var d = Object.getOwnPropertyDescriptor(e, k);
13
+ Object.defineProperty(n, k, d.get ? d : {
14
+ enumerable: true,
15
+ get: function () { return e[k]; }
16
+ });
17
+ }
18
+ });
19
+ }
20
+ n.default = e;
21
+ return Object.freeze(n);
22
+ }
23
+
24
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
25
+
26
+ // src/provider.tsx
27
+
28
+ // src/types.ts
29
+ var DEFAULT_CONFIG = {
30
+ authOrigin: "https://ochk.io",
31
+ signInPath: "/signin",
32
+ mePath: "/api/auth/me",
33
+ logoutPath: "/api/auth/logout"
34
+ };
35
+ function resolveConfig(cfg) {
36
+ return { ...DEFAULT_CONFIG, ...cfg ?? {} };
37
+ }
38
+ function buildSignInUrl(cfg, returnTo) {
39
+ const base = `${cfg.authOrigin}${cfg.signInPath}`;
40
+ if (!returnTo) return base;
41
+ const u = new URL(base);
42
+ u.searchParams.set("return_to", returnTo);
43
+ return u.toString();
44
+ }
45
+ var SessionContext = React__namespace.createContext(null);
46
+ function normalizeAccount(raw) {
47
+ if (!raw) return null;
48
+ const address = raw.btc_address ?? raw.address;
49
+ const accountId = raw.id ?? raw.account_id ?? raw.accountId;
50
+ if (!address || !accountId) return null;
51
+ return {
52
+ accountId,
53
+ address,
54
+ displayName: raw.display_name ?? raw.displayName ?? null,
55
+ nostrNpub: raw.nostr_npub ?? raw.nostrNpub ?? null
56
+ };
57
+ }
58
+ function OcSessionProvider({
59
+ children,
60
+ config,
61
+ defaultReturnTo
62
+ }) {
63
+ const cfg = React__namespace.useMemo(() => resolveConfig(config), [config]);
64
+ const [account, setAccount] = React__namespace.useState(null);
65
+ const [status, setStatus] = React__namespace.useState("loading");
66
+ const [error, setError] = React__namespace.useState(null);
67
+ const refresh = React__namespace.useCallback(async () => {
68
+ if (typeof window === "undefined") return;
69
+ try {
70
+ const res = await fetch(cfg.mePath, {
71
+ method: "GET",
72
+ credentials: "include",
73
+ headers: { Accept: "application/json" }
74
+ });
75
+ if (res.status === 401) {
76
+ setAccount(null);
77
+ setStatus("anonymous");
78
+ setError(null);
79
+ return;
80
+ }
81
+ if (!res.ok) {
82
+ setStatus("error");
83
+ setError(new Error(`me endpoint returned ${res.status}`));
84
+ return;
85
+ }
86
+ const body = await res.json();
87
+ const acct = normalizeAccount(body.account);
88
+ setAccount(acct);
89
+ setStatus(acct ? "authenticated" : "anonymous");
90
+ setError(null);
91
+ } catch (err) {
92
+ setStatus("error");
93
+ setError(err instanceof Error ? err : new Error(String(err)));
94
+ }
95
+ }, [cfg.mePath]);
96
+ React__namespace.useEffect(() => {
97
+ void refresh();
98
+ }, [refresh]);
99
+ const signOut = React__namespace.useCallback(async () => {
100
+ try {
101
+ await fetch(`${cfg.authOrigin}${cfg.logoutPath}`, {
102
+ method: "POST",
103
+ credentials: "include"
104
+ });
105
+ } catch {
106
+ }
107
+ setAccount(null);
108
+ setStatus("anonymous");
109
+ }, [cfg.authOrigin, cfg.logoutPath]);
110
+ const value = React__namespace.useMemo(() => {
111
+ const returnTo = defaultReturnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
112
+ return {
113
+ status,
114
+ account,
115
+ error,
116
+ refresh,
117
+ signOut,
118
+ signInUrl: buildSignInUrl(cfg, returnTo)
119
+ };
120
+ }, [status, account, error, refresh, signOut, cfg, defaultReturnTo]);
121
+ return /* @__PURE__ */ jsxRuntime.jsx(SessionContext.Provider, { value, children });
122
+ }
123
+ function useOcSession() {
124
+ const ctx = React__namespace.useContext(SessionContext);
125
+ if (!ctx) {
126
+ throw new Error(
127
+ "[@orangecheck/auth-client] useOcSession() must be called inside <OcSessionProvider>"
128
+ );
129
+ }
130
+ return ctx;
131
+ }
132
+ function useOptionalOcSession() {
133
+ return React__namespace.useContext(SessionContext);
134
+ }
135
+ function shortenAddress(addr) {
136
+ if (addr.length <= 12) return addr;
137
+ return `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`;
138
+ }
139
+ function OcSignInButton({
140
+ label = "sign in with bitcoin",
141
+ eager = false,
142
+ className,
143
+ ...rest
144
+ }) {
145
+ const { status, signInUrl } = useOcSession();
146
+ if (status === "authenticated") return null;
147
+ if (!eager && status === "loading") return null;
148
+ return /* @__PURE__ */ jsxRuntime.jsx(
149
+ "a",
150
+ {
151
+ ...rest,
152
+ href: signInUrl,
153
+ className,
154
+ "data-oc-sign-in-button": "",
155
+ children: label
156
+ }
157
+ );
158
+ }
159
+ function OcAccountPill({
160
+ dashboardUrl,
161
+ render,
162
+ className,
163
+ ...rest
164
+ }) {
165
+ const { status, account, signOut } = useOcSession();
166
+ if (status !== "authenticated" || !account) return null;
167
+ const label = render ? render({ address: account.address, displayName: account.displayName }) : account.displayName ?? shortenAddress(account.address);
168
+ return /* @__PURE__ */ jsxRuntime.jsxs(
169
+ "div",
170
+ {
171
+ ...rest,
172
+ className,
173
+ "data-oc-account-pill": "",
174
+ style: { display: "inline-flex", alignItems: "center", gap: "0.5rem", ...rest.style ?? {} },
175
+ children: [
176
+ dashboardUrl ? /* @__PURE__ */ jsxRuntime.jsx("a", { href: dashboardUrl, children: label }) : /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }),
177
+ /* @__PURE__ */ jsxRuntime.jsx(
178
+ "button",
179
+ {
180
+ type: "button",
181
+ onClick: () => {
182
+ void signOut();
183
+ },
184
+ "aria-label": "Sign out",
185
+ style: {
186
+ background: "none",
187
+ border: "none",
188
+ cursor: "pointer",
189
+ color: "inherit",
190
+ font: "inherit",
191
+ padding: 0
192
+ },
193
+ children: "sign out"
194
+ }
195
+ )
196
+ ]
197
+ }
198
+ );
199
+ }
200
+
201
+ exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
202
+ exports.OcAccountPill = OcAccountPill;
203
+ exports.OcSessionProvider = OcSessionProvider;
204
+ exports.OcSignInButton = OcSignInButton;
205
+ exports.buildSignInUrl = buildSignInUrl;
206
+ exports.useOcSession = useOcSession;
207
+ exports.useOptionalOcSession = useOptionalOcSession;
208
+ //# sourceMappingURL=index.js.map
209
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/provider.tsx","../src/components.tsx"],"names":["React","jsx","jsxs"],"mappings":"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","file":"index.js","sourcesContent":["export interface OcAccount {\n accountId: string;\n address: string;\n displayName?: string | null;\n nostrNpub?: string | null;\n}\n\nexport type OcSessionStatus = 'loading' | 'authenticated' | 'anonymous' | 'error';\n\nexport interface OcSessionState {\n status: OcSessionStatus;\n account: OcAccount | null;\n /** `null` while loading; an `Error` instance when `status === 'error'`. */\n error: Error | null;\n /** Re-fetch the session. Useful after sign-in/sign-out happens elsewhere. */\n refresh: () => Promise<void>;\n /** Trigger a sign-out. Resolves once the cookie has been cleared. */\n signOut: () => Promise<void>;\n /** URL to navigate to for sign-in on the auth host. */\n signInUrl: string;\n}\n\nexport interface OcAuthConfig {\n /**\n * Origin of the auth host — the subdomain that runs the sign-in UI,\n * issues session cookies, and exposes `/api/auth/me` + `/api/auth/logout`.\n *\n * Defaults to `https://ochk.io`. Override in preview/dev.\n */\n authOrigin?: string;\n /**\n * Path on the auth host that accepts `?return_to=<url>` and drives the\n * BIP-322 sign-in flow. Defaults to `/signin`.\n */\n signInPath?: string;\n /**\n * Local path (same origin as the current app) that exposes the\n * crypto-verified session. If your app ships one at `/api/auth/me`,\n * leave as default. Returns 200 `{ account }` or 401.\n */\n mePath?: string;\n /**\n * Path on the auth host to hit to clear the session cookie.\n * Defaults to `/api/auth/logout`. Called with `credentials: 'include'`\n * so the `.ochk.io` cookie is sent along.\n */\n logoutPath?: string;\n}\n\nexport const DEFAULT_CONFIG: Required<OcAuthConfig> = {\n authOrigin: 'https://ochk.io',\n signInPath: '/signin',\n mePath: '/api/auth/me',\n logoutPath: '/api/auth/logout',\n};\n\nexport function resolveConfig(cfg: OcAuthConfig | undefined): Required<OcAuthConfig> {\n return { ...DEFAULT_CONFIG, ...(cfg ?? {}) };\n}\n\nexport function buildSignInUrl(cfg: Required<OcAuthConfig>, returnTo?: string): string {\n const base = `${cfg.authOrigin}${cfg.signInPath}`;\n if (!returnTo) return base;\n const u = new URL(base);\n u.searchParams.set('return_to', returnTo);\n return u.toString();\n}\n","import * as React from 'react';\n\nimport {\n buildSignInUrl,\n DEFAULT_CONFIG,\n resolveConfig,\n type OcAccount,\n type OcAuthConfig,\n type OcSessionState,\n} from './types';\n\nconst SessionContext = React.createContext<OcSessionState | null>(null);\n\ninterface MeResponse {\n account?: {\n id?: string;\n account_id?: string;\n accountId?: string;\n btc_address?: string;\n address?: string;\n display_name?: string | null;\n displayName?: string | null;\n nostr_npub?: string | null;\n nostrNpub?: string | null;\n };\n}\n\nfunction normalizeAccount(raw: MeResponse['account']): OcAccount | null {\n if (!raw) return null;\n const address = raw.btc_address ?? raw.address;\n const accountId = raw.id ?? raw.account_id ?? raw.accountId;\n if (!address || !accountId) return null;\n return {\n accountId,\n address,\n displayName: raw.display_name ?? raw.displayName ?? null,\n nostrNpub: raw.nostr_npub ?? raw.nostrNpub ?? null,\n };\n}\n\nexport interface OcSessionProviderProps {\n children: React.ReactNode;\n config?: OcAuthConfig;\n /**\n * Optional return URL passed to the sign-in page. Defaults to the\n * current `window.location.href` at click-time.\n */\n defaultReturnTo?: string;\n}\n\n/**\n * Top-level provider that exposes the cross-subdomain oc_session to every\n * component below it. Mount once, near the root of your tree.\n */\nexport function OcSessionProvider({\n children,\n config,\n defaultReturnTo,\n}: OcSessionProviderProps): React.ReactElement {\n const cfg = React.useMemo(() => resolveConfig(config), [config]);\n const [account, setAccount] = React.useState<OcAccount | null>(null);\n const [status, setStatus] = React.useState<OcSessionState['status']>('loading');\n const [error, setError] = React.useState<Error | null>(null);\n\n const refresh = React.useCallback(async () => {\n if (typeof window === 'undefined') return;\n try {\n const res = await fetch(cfg.mePath, {\n method: 'GET',\n credentials: 'include',\n headers: { Accept: 'application/json' },\n });\n if (res.status === 401) {\n setAccount(null);\n setStatus('anonymous');\n setError(null);\n return;\n }\n if (!res.ok) {\n setStatus('error');\n setError(new Error(`me endpoint returned ${res.status}`));\n return;\n }\n const body = (await res.json()) as MeResponse;\n const acct = normalizeAccount(body.account);\n setAccount(acct);\n setStatus(acct ? 'authenticated' : 'anonymous');\n setError(null);\n } catch (err) {\n setStatus('error');\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [cfg.mePath]);\n\n React.useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const signOut = React.useCallback(async () => {\n try {\n await fetch(`${cfg.authOrigin}${cfg.logoutPath}`, {\n method: 'POST',\n credentials: 'include',\n });\n } catch {\n // fall through — we still clear local state so the UI reflects\n // the user's intent even if the server round-trip fails.\n }\n setAccount(null);\n setStatus('anonymous');\n }, [cfg.authOrigin, cfg.logoutPath]);\n\n const value = React.useMemo<OcSessionState>(() => {\n const returnTo =\n defaultReturnTo ?? (typeof window !== 'undefined' ? window.location.href : undefined);\n return {\n status,\n account,\n error,\n refresh,\n signOut,\n signInUrl: buildSignInUrl(cfg, returnTo),\n };\n }, [status, account, error, refresh, signOut, cfg, defaultReturnTo]);\n\n return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;\n}\n\n/**\n * Access the current cross-subdomain oc_session. Must be called inside\n * an `<OcSessionProvider>`.\n */\nexport function useOcSession(): OcSessionState {\n const ctx = React.useContext(SessionContext);\n if (!ctx) {\n throw new Error(\n '[@orangecheck/auth-client] useOcSession() must be called inside <OcSessionProvider>'\n );\n }\n return ctx;\n}\n\n/**\n * Non-throwing variant — returns `null` if called outside a provider.\n * Useful for libraries that want to read the session *if it exists* but\n * shouldn't crash on apps that haven't opted in.\n */\nexport function useOptionalOcSession(): OcSessionState | null {\n return React.useContext(SessionContext);\n}\n\nexport { DEFAULT_CONFIG };\n","import * as React from 'react';\n\nimport { useOcSession } from './provider';\n\nfunction shortenAddress(addr: string): string {\n if (addr.length <= 12) return addr;\n return `${addr.slice(0, 6)}…${addr.slice(-4)}`;\n}\n\nexport interface OcSignInButtonProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\n /** Label shown when no user is signed in. Defaults to `sign in with bitcoin`. */\n label?: string;\n /**\n * When `true`, render an `<a>` even while the session is loading, to\n * avoid layout shift. Defaults to `false` (renders nothing while loading).\n */\n eager?: boolean;\n}\n\n/**\n * Drop-in sign-in button. Renders an anchor that deep-links to the auth\n * host's sign-in page with the current URL as `?return_to=…`.\n *\n * When the user is already authenticated it renders nothing — wrap it in\n * a conditional or use `<OcAccountPill>` as the signed-in affordance.\n */\nexport function OcSignInButton({\n label = 'sign in with bitcoin',\n eager = false,\n className,\n ...rest\n}: OcSignInButtonProps): React.ReactElement | null {\n const { status, signInUrl } = useOcSession();\n if (status === 'authenticated') return null;\n if (!eager && status === 'loading') return null;\n\n return (\n <a\n {...rest}\n href={signInUrl}\n className={className}\n data-oc-sign-in-button=\"\"\n >\n {label}\n </a>\n );\n}\n\nexport interface OcAccountPillProps extends React.HTMLAttributes<HTMLDivElement> {\n /** URL to link the address to. Defaults to the auth origin's `/dashboard`. */\n dashboardUrl?: string;\n /** Override the display text. Defaults to the shortened address. */\n render?: (account: { address: string; displayName?: string | null }) => React.ReactNode;\n}\n\n/**\n * Shows the signed-in user as a short pill: `bc1q…abcd sign out`.\n *\n * Renders nothing while loading or when no user is signed in — pair with\n * `<OcSignInButton>` for the anonymous case.\n */\nexport function OcAccountPill({\n dashboardUrl,\n render,\n className,\n ...rest\n}: OcAccountPillProps): React.ReactElement | null {\n const { status, account, signOut } = useOcSession();\n\n if (status !== 'authenticated' || !account) return null;\n\n const label = render\n ? render({ address: account.address, displayName: account.displayName })\n : (account.displayName ?? shortenAddress(account.address));\n\n return (\n <div\n {...rest}\n className={className}\n data-oc-account-pill=\"\"\n style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', ...(rest.style ?? {}) }}\n >\n {dashboardUrl ? (\n <a href={dashboardUrl}>{label}</a>\n ) : (\n <span>{label}</span>\n )}\n <button\n type=\"button\"\n onClick={() => {\n void signOut();\n }}\n aria-label=\"Sign out\"\n style={{\n background: 'none',\n border: 'none',\n cursor: 'pointer',\n color: 'inherit',\n font: 'inherit',\n padding: 0,\n }}\n >\n sign out\n </button>\n </div>\n );\n}\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,181 @@
1
+ import * as React from 'react';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+
4
+ // src/provider.tsx
5
+
6
+ // src/types.ts
7
+ var DEFAULT_CONFIG = {
8
+ authOrigin: "https://ochk.io",
9
+ signInPath: "/signin",
10
+ mePath: "/api/auth/me",
11
+ logoutPath: "/api/auth/logout"
12
+ };
13
+ function resolveConfig(cfg) {
14
+ return { ...DEFAULT_CONFIG, ...cfg ?? {} };
15
+ }
16
+ function buildSignInUrl(cfg, returnTo) {
17
+ const base = `${cfg.authOrigin}${cfg.signInPath}`;
18
+ if (!returnTo) return base;
19
+ const u = new URL(base);
20
+ u.searchParams.set("return_to", returnTo);
21
+ return u.toString();
22
+ }
23
+ var SessionContext = React.createContext(null);
24
+ function normalizeAccount(raw) {
25
+ if (!raw) return null;
26
+ const address = raw.btc_address ?? raw.address;
27
+ const accountId = raw.id ?? raw.account_id ?? raw.accountId;
28
+ if (!address || !accountId) return null;
29
+ return {
30
+ accountId,
31
+ address,
32
+ displayName: raw.display_name ?? raw.displayName ?? null,
33
+ nostrNpub: raw.nostr_npub ?? raw.nostrNpub ?? null
34
+ };
35
+ }
36
+ function OcSessionProvider({
37
+ children,
38
+ config,
39
+ defaultReturnTo
40
+ }) {
41
+ const cfg = React.useMemo(() => resolveConfig(config), [config]);
42
+ const [account, setAccount] = React.useState(null);
43
+ const [status, setStatus] = React.useState("loading");
44
+ const [error, setError] = React.useState(null);
45
+ const refresh = React.useCallback(async () => {
46
+ if (typeof window === "undefined") return;
47
+ try {
48
+ const res = await fetch(cfg.mePath, {
49
+ method: "GET",
50
+ credentials: "include",
51
+ headers: { Accept: "application/json" }
52
+ });
53
+ if (res.status === 401) {
54
+ setAccount(null);
55
+ setStatus("anonymous");
56
+ setError(null);
57
+ return;
58
+ }
59
+ if (!res.ok) {
60
+ setStatus("error");
61
+ setError(new Error(`me endpoint returned ${res.status}`));
62
+ return;
63
+ }
64
+ const body = await res.json();
65
+ const acct = normalizeAccount(body.account);
66
+ setAccount(acct);
67
+ setStatus(acct ? "authenticated" : "anonymous");
68
+ setError(null);
69
+ } catch (err) {
70
+ setStatus("error");
71
+ setError(err instanceof Error ? err : new Error(String(err)));
72
+ }
73
+ }, [cfg.mePath]);
74
+ React.useEffect(() => {
75
+ void refresh();
76
+ }, [refresh]);
77
+ const signOut = React.useCallback(async () => {
78
+ try {
79
+ await fetch(`${cfg.authOrigin}${cfg.logoutPath}`, {
80
+ method: "POST",
81
+ credentials: "include"
82
+ });
83
+ } catch {
84
+ }
85
+ setAccount(null);
86
+ setStatus("anonymous");
87
+ }, [cfg.authOrigin, cfg.logoutPath]);
88
+ const value = React.useMemo(() => {
89
+ const returnTo = defaultReturnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
90
+ return {
91
+ status,
92
+ account,
93
+ error,
94
+ refresh,
95
+ signOut,
96
+ signInUrl: buildSignInUrl(cfg, returnTo)
97
+ };
98
+ }, [status, account, error, refresh, signOut, cfg, defaultReturnTo]);
99
+ return /* @__PURE__ */ jsx(SessionContext.Provider, { value, children });
100
+ }
101
+ function useOcSession() {
102
+ const ctx = React.useContext(SessionContext);
103
+ if (!ctx) {
104
+ throw new Error(
105
+ "[@orangecheck/auth-client] useOcSession() must be called inside <OcSessionProvider>"
106
+ );
107
+ }
108
+ return ctx;
109
+ }
110
+ function useOptionalOcSession() {
111
+ return React.useContext(SessionContext);
112
+ }
113
+ function shortenAddress(addr) {
114
+ if (addr.length <= 12) return addr;
115
+ return `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`;
116
+ }
117
+ function OcSignInButton({
118
+ label = "sign in with bitcoin",
119
+ eager = false,
120
+ className,
121
+ ...rest
122
+ }) {
123
+ const { status, signInUrl } = useOcSession();
124
+ if (status === "authenticated") return null;
125
+ if (!eager && status === "loading") return null;
126
+ return /* @__PURE__ */ jsx(
127
+ "a",
128
+ {
129
+ ...rest,
130
+ href: signInUrl,
131
+ className,
132
+ "data-oc-sign-in-button": "",
133
+ children: label
134
+ }
135
+ );
136
+ }
137
+ function OcAccountPill({
138
+ dashboardUrl,
139
+ render,
140
+ className,
141
+ ...rest
142
+ }) {
143
+ const { status, account, signOut } = useOcSession();
144
+ if (status !== "authenticated" || !account) return null;
145
+ const label = render ? render({ address: account.address, displayName: account.displayName }) : account.displayName ?? shortenAddress(account.address);
146
+ return /* @__PURE__ */ jsxs(
147
+ "div",
148
+ {
149
+ ...rest,
150
+ className,
151
+ "data-oc-account-pill": "",
152
+ style: { display: "inline-flex", alignItems: "center", gap: "0.5rem", ...rest.style ?? {} },
153
+ children: [
154
+ dashboardUrl ? /* @__PURE__ */ jsx("a", { href: dashboardUrl, children: label }) : /* @__PURE__ */ jsx("span", { children: label }),
155
+ /* @__PURE__ */ jsx(
156
+ "button",
157
+ {
158
+ type: "button",
159
+ onClick: () => {
160
+ void signOut();
161
+ },
162
+ "aria-label": "Sign out",
163
+ style: {
164
+ background: "none",
165
+ border: "none",
166
+ cursor: "pointer",
167
+ color: "inherit",
168
+ font: "inherit",
169
+ padding: 0
170
+ },
171
+ children: "sign out"
172
+ }
173
+ )
174
+ ]
175
+ }
176
+ );
177
+ }
178
+
179
+ export { DEFAULT_CONFIG, OcAccountPill, OcSessionProvider, OcSignInButton, buildSignInUrl, useOcSession, useOptionalOcSession };
180
+ //# sourceMappingURL=index.mjs.map
181
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/provider.tsx","../src/components.tsx"],"names":["jsx"],"mappings":"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","file":"index.mjs","sourcesContent":["export interface OcAccount {\n accountId: string;\n address: string;\n displayName?: string | null;\n nostrNpub?: string | null;\n}\n\nexport type OcSessionStatus = 'loading' | 'authenticated' | 'anonymous' | 'error';\n\nexport interface OcSessionState {\n status: OcSessionStatus;\n account: OcAccount | null;\n /** `null` while loading; an `Error` instance when `status === 'error'`. */\n error: Error | null;\n /** Re-fetch the session. Useful after sign-in/sign-out happens elsewhere. */\n refresh: () => Promise<void>;\n /** Trigger a sign-out. Resolves once the cookie has been cleared. */\n signOut: () => Promise<void>;\n /** URL to navigate to for sign-in on the auth host. */\n signInUrl: string;\n}\n\nexport interface OcAuthConfig {\n /**\n * Origin of the auth host — the subdomain that runs the sign-in UI,\n * issues session cookies, and exposes `/api/auth/me` + `/api/auth/logout`.\n *\n * Defaults to `https://ochk.io`. Override in preview/dev.\n */\n authOrigin?: string;\n /**\n * Path on the auth host that accepts `?return_to=<url>` and drives the\n * BIP-322 sign-in flow. Defaults to `/signin`.\n */\n signInPath?: string;\n /**\n * Local path (same origin as the current app) that exposes the\n * crypto-verified session. If your app ships one at `/api/auth/me`,\n * leave as default. Returns 200 `{ account }` or 401.\n */\n mePath?: string;\n /**\n * Path on the auth host to hit to clear the session cookie.\n * Defaults to `/api/auth/logout`. Called with `credentials: 'include'`\n * so the `.ochk.io` cookie is sent along.\n */\n logoutPath?: string;\n}\n\nexport const DEFAULT_CONFIG: Required<OcAuthConfig> = {\n authOrigin: 'https://ochk.io',\n signInPath: '/signin',\n mePath: '/api/auth/me',\n logoutPath: '/api/auth/logout',\n};\n\nexport function resolveConfig(cfg: OcAuthConfig | undefined): Required<OcAuthConfig> {\n return { ...DEFAULT_CONFIG, ...(cfg ?? {}) };\n}\n\nexport function buildSignInUrl(cfg: Required<OcAuthConfig>, returnTo?: string): string {\n const base = `${cfg.authOrigin}${cfg.signInPath}`;\n if (!returnTo) return base;\n const u = new URL(base);\n u.searchParams.set('return_to', returnTo);\n return u.toString();\n}\n","import * as React from 'react';\n\nimport {\n buildSignInUrl,\n DEFAULT_CONFIG,\n resolveConfig,\n type OcAccount,\n type OcAuthConfig,\n type OcSessionState,\n} from './types';\n\nconst SessionContext = React.createContext<OcSessionState | null>(null);\n\ninterface MeResponse {\n account?: {\n id?: string;\n account_id?: string;\n accountId?: string;\n btc_address?: string;\n address?: string;\n display_name?: string | null;\n displayName?: string | null;\n nostr_npub?: string | null;\n nostrNpub?: string | null;\n };\n}\n\nfunction normalizeAccount(raw: MeResponse['account']): OcAccount | null {\n if (!raw) return null;\n const address = raw.btc_address ?? raw.address;\n const accountId = raw.id ?? raw.account_id ?? raw.accountId;\n if (!address || !accountId) return null;\n return {\n accountId,\n address,\n displayName: raw.display_name ?? raw.displayName ?? null,\n nostrNpub: raw.nostr_npub ?? raw.nostrNpub ?? null,\n };\n}\n\nexport interface OcSessionProviderProps {\n children: React.ReactNode;\n config?: OcAuthConfig;\n /**\n * Optional return URL passed to the sign-in page. Defaults to the\n * current `window.location.href` at click-time.\n */\n defaultReturnTo?: string;\n}\n\n/**\n * Top-level provider that exposes the cross-subdomain oc_session to every\n * component below it. Mount once, near the root of your tree.\n */\nexport function OcSessionProvider({\n children,\n config,\n defaultReturnTo,\n}: OcSessionProviderProps): React.ReactElement {\n const cfg = React.useMemo(() => resolveConfig(config), [config]);\n const [account, setAccount] = React.useState<OcAccount | null>(null);\n const [status, setStatus] = React.useState<OcSessionState['status']>('loading');\n const [error, setError] = React.useState<Error | null>(null);\n\n const refresh = React.useCallback(async () => {\n if (typeof window === 'undefined') return;\n try {\n const res = await fetch(cfg.mePath, {\n method: 'GET',\n credentials: 'include',\n headers: { Accept: 'application/json' },\n });\n if (res.status === 401) {\n setAccount(null);\n setStatus('anonymous');\n setError(null);\n return;\n }\n if (!res.ok) {\n setStatus('error');\n setError(new Error(`me endpoint returned ${res.status}`));\n return;\n }\n const body = (await res.json()) as MeResponse;\n const acct = normalizeAccount(body.account);\n setAccount(acct);\n setStatus(acct ? 'authenticated' : 'anonymous');\n setError(null);\n } catch (err) {\n setStatus('error');\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n }, [cfg.mePath]);\n\n React.useEffect(() => {\n void refresh();\n }, [refresh]);\n\n const signOut = React.useCallback(async () => {\n try {\n await fetch(`${cfg.authOrigin}${cfg.logoutPath}`, {\n method: 'POST',\n credentials: 'include',\n });\n } catch {\n // fall through — we still clear local state so the UI reflects\n // the user's intent even if the server round-trip fails.\n }\n setAccount(null);\n setStatus('anonymous');\n }, [cfg.authOrigin, cfg.logoutPath]);\n\n const value = React.useMemo<OcSessionState>(() => {\n const returnTo =\n defaultReturnTo ?? (typeof window !== 'undefined' ? window.location.href : undefined);\n return {\n status,\n account,\n error,\n refresh,\n signOut,\n signInUrl: buildSignInUrl(cfg, returnTo),\n };\n }, [status, account, error, refresh, signOut, cfg, defaultReturnTo]);\n\n return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;\n}\n\n/**\n * Access the current cross-subdomain oc_session. Must be called inside\n * an `<OcSessionProvider>`.\n */\nexport function useOcSession(): OcSessionState {\n const ctx = React.useContext(SessionContext);\n if (!ctx) {\n throw new Error(\n '[@orangecheck/auth-client] useOcSession() must be called inside <OcSessionProvider>'\n );\n }\n return ctx;\n}\n\n/**\n * Non-throwing variant — returns `null` if called outside a provider.\n * Useful for libraries that want to read the session *if it exists* but\n * shouldn't crash on apps that haven't opted in.\n */\nexport function useOptionalOcSession(): OcSessionState | null {\n return React.useContext(SessionContext);\n}\n\nexport { DEFAULT_CONFIG };\n","import * as React from 'react';\n\nimport { useOcSession } from './provider';\n\nfunction shortenAddress(addr: string): string {\n if (addr.length <= 12) return addr;\n return `${addr.slice(0, 6)}…${addr.slice(-4)}`;\n}\n\nexport interface OcSignInButtonProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\n /** Label shown when no user is signed in. Defaults to `sign in with bitcoin`. */\n label?: string;\n /**\n * When `true`, render an `<a>` even while the session is loading, to\n * avoid layout shift. Defaults to `false` (renders nothing while loading).\n */\n eager?: boolean;\n}\n\n/**\n * Drop-in sign-in button. Renders an anchor that deep-links to the auth\n * host's sign-in page with the current URL as `?return_to=…`.\n *\n * When the user is already authenticated it renders nothing — wrap it in\n * a conditional or use `<OcAccountPill>` as the signed-in affordance.\n */\nexport function OcSignInButton({\n label = 'sign in with bitcoin',\n eager = false,\n className,\n ...rest\n}: OcSignInButtonProps): React.ReactElement | null {\n const { status, signInUrl } = useOcSession();\n if (status === 'authenticated') return null;\n if (!eager && status === 'loading') return null;\n\n return (\n <a\n {...rest}\n href={signInUrl}\n className={className}\n data-oc-sign-in-button=\"\"\n >\n {label}\n </a>\n );\n}\n\nexport interface OcAccountPillProps extends React.HTMLAttributes<HTMLDivElement> {\n /** URL to link the address to. Defaults to the auth origin's `/dashboard`. */\n dashboardUrl?: string;\n /** Override the display text. Defaults to the shortened address. */\n render?: (account: { address: string; displayName?: string | null }) => React.ReactNode;\n}\n\n/**\n * Shows the signed-in user as a short pill: `bc1q…abcd sign out`.\n *\n * Renders nothing while loading or when no user is signed in — pair with\n * `<OcSignInButton>` for the anonymous case.\n */\nexport function OcAccountPill({\n dashboardUrl,\n render,\n className,\n ...rest\n}: OcAccountPillProps): React.ReactElement | null {\n const { status, account, signOut } = useOcSession();\n\n if (status !== 'authenticated' || !account) return null;\n\n const label = render\n ? render({ address: account.address, displayName: account.displayName })\n : (account.displayName ?? shortenAddress(account.address));\n\n return (\n <div\n {...rest}\n className={className}\n data-oc-account-pill=\"\"\n style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', ...(rest.style ?? {}) }}\n >\n {dashboardUrl ? (\n <a href={dashboardUrl}>{label}</a>\n ) : (\n <span>{label}</span>\n )}\n <button\n type=\"button\"\n onClick={() => {\n void signOut();\n }}\n aria-label=\"Sign out\"\n style={{\n background: 'none',\n border: 'none',\n cursor: 'pointer',\n color: 'inherit',\n font: 'inherit',\n padding: 0,\n }}\n >\n sign out\n </button>\n </div>\n );\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@orangecheck/auth-client",
3
+ "version": "0.1.0",
4
+ "description": "React hooks and components for the cross-subdomain oc_session. Drop-in sign-in button, account pill, and useOcSession() hook.",
5
+ "keywords": [
6
+ "orangecheck",
7
+ "bitcoin",
8
+ "auth",
9
+ "react",
10
+ "sso",
11
+ "ed25519"
12
+ ],
13
+ "author": "OrangeCheck",
14
+ "license": "MIT",
15
+ "homepage": "https://ochk.io",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/orangecheck/oc-packages.git",
19
+ "directory": "auth-client"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/orangecheck/oc-packages/issues"
23
+ },
24
+ "main": "./dist/index.js",
25
+ "module": "./dist/index.mjs",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.mjs",
31
+ "require": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "test": "vitest run",
43
+ "type-check": "tsc --noEmit",
44
+ "clean": "rm -rf dist",
45
+ "prepublishOnly": "npm run clean && npm run build"
46
+ },
47
+ "peerDependencies": {
48
+ "@orangecheck/auth-core": "^0.1.0",
49
+ "react": "^18.0.0 || ^19.0.0",
50
+ "react-dom": "^18.0.0 || ^19.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@orangecheck/auth-core": "^0.1.0",
54
+ "@testing-library/react": "^16.3.2",
55
+ "@types/node": "^22.0.0",
56
+ "@types/react": "^18.3.12",
57
+ "@types/react-dom": "^18.3.1",
58
+ "jsdom": "^29.0.2",
59
+ "react": "^18.3.1",
60
+ "react-dom": "^18.3.1",
61
+ "tsup": "^8.3.5",
62
+ "typescript": "^5.6.3",
63
+ "vitest": "^3.0.0"
64
+ }
65
+ }