@elvix.is/sdk 0.1.1 → 0.2.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/README.md CHANGED
@@ -186,6 +186,10 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md). PRs welcome — we run CI on every pus
186
186
 
187
187
  ## Maintained by
188
188
 
189
- **[edvone](https://edvone.dev)** · Aachen, Germany · [hi@edvone.dev](mailto:hi@edvone.dev)
189
+ **[edvone](https://edvone.dev)** · Aachen, Germany
190
190
 
191
191
  elvix is an edvone product.
192
+
193
+ - General enquiries: [edvone.dev/contact](https://edvone.dev/contact)
194
+ - Sales / integration call: [edvone.dev/book](https://edvone.dev/book)
195
+ - Security disclosure: [security@elvix.is](mailto:security@elvix.is) (see [SECURITY.md](./SECURITY.md))
package/dist/react.d.ts CHANGED
@@ -1 +1,104 @@
1
- export { ElvixActionResult, ElvixUser, ElvixVerifyErr, ElvixVerifyOk, ElvixVerifyResult } from './index.js';
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * Public types for the React surface. Mirrors the elvix.is bootstrap
6
+ * envelope so customers can type their host code without importing
7
+ * private elvix internals.
8
+ */
9
+ type ElvixBrand = {
10
+ light: {
11
+ primary: string;
12
+ on: string;
13
+ };
14
+ dark: {
15
+ primary: string;
16
+ on: string;
17
+ };
18
+ };
19
+ type ElvixSignInMethod = "google" | "email_otp" | "passkey" | "username";
20
+ type ElvixBootstrapEnvelope = {
21
+ applicationId: string;
22
+ clientId: string;
23
+ urlSlug: string;
24
+ appName: string;
25
+ logoUrl: string | null;
26
+ logoUrlDark: string | null;
27
+ iconUrl: string | null;
28
+ iconUrlDark: string | null;
29
+ brand: ElvixBrand;
30
+ methods: {
31
+ google: boolean;
32
+ emailOtp: boolean;
33
+ passkey: boolean;
34
+ username: boolean;
35
+ };
36
+ legal: {
37
+ privacyPolicyUrl: string;
38
+ termsOfServiceUrl: string;
39
+ supportEmail: string;
40
+ supportUrl: string | null;
41
+ };
42
+ signInVerb: "signin" | "login";
43
+ signinGate: "public" | "private_beta" | "closed";
44
+ };
45
+ type ElvixSignInResultOk = {
46
+ ok: true;
47
+ /** Where the host should send the user (if anywhere). */
48
+ redirect?: string;
49
+ /** Sign-in factor that succeeded. */
50
+ method: ElvixSignInMethod;
51
+ };
52
+ type ElvixSignInResultErr = {
53
+ ok: false;
54
+ error: string;
55
+ message?: string;
56
+ };
57
+ type ElvixSignInResult = ElvixSignInResultOk | ElvixSignInResultErr;
58
+ /** Theme override. Omit to inherit the Console-configured pair. */
59
+ type ElvixTheme = "light" | "dark" | "system";
60
+
61
+ type ElvixContextValue = {
62
+ clientId: string | undefined;
63
+ baseUrl: string;
64
+ app: ElvixBootstrapEnvelope | null;
65
+ appError: string | null;
66
+ resolvedTheme: "light" | "dark";
67
+ };
68
+ declare function useElvixApp(): ElvixBootstrapEnvelope | null;
69
+ declare function useElvixContext(): ElvixContextValue;
70
+ declare function ElvixProvider({ clientId, theme, brand, baseUrl, children, className, }: {
71
+ clientId?: string;
72
+ theme?: ElvixTheme;
73
+ brand?: ElvixBrand;
74
+ /** Override the elvix origin (testing, proxy setups). */
75
+ baseUrl?: string;
76
+ children: ReactNode;
77
+ className?: string;
78
+ }): react.JSX.Element;
79
+
80
+ /**
81
+ * `<ElvixSignIn>` — drop-in sign-in surface.
82
+ *
83
+ * Reads enabled methods from the Console-configured bootstrap envelope
84
+ * (`<ElvixProvider clientId>` must be in scope). Renders the methods
85
+ * the customer turned on; never invents UI the Console denied.
86
+ *
87
+ * MVP supports:
88
+ * - Email OTP (the most common path)
89
+ * - Google redirect ("Continue with Google")
90
+ *
91
+ * Passkey + username-OTP follow in 0.2.x point releases.
92
+ *
93
+ * `onResult({ ok, ... })` is the only post-success hook the SDK
94
+ * exposes. Hosts navigate from the callback; this component never
95
+ * calls `router.push` itself.
96
+ */
97
+ declare function ElvixSignIn({ onResult, redirectAfterSignIn, className, }: {
98
+ onResult?: (r: ElvixSignInResult) => void;
99
+ /** Default redirect target on success when the server doesn't echo one. */
100
+ redirectAfterSignIn?: string;
101
+ className?: string;
102
+ }): react.JSX.Element;
103
+
104
+ export { type ElvixBootstrapEnvelope, type ElvixBrand, ElvixProvider, ElvixSignIn, type ElvixSignInMethod, type ElvixSignInResult, type ElvixSignInResultErr, type ElvixSignInResultOk, type ElvixTheme, useElvixApp, useElvixContext };
package/dist/react.js CHANGED
@@ -0,0 +1,245 @@
1
+ // src/react/elvix-provider.tsx
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useState
8
+ } from "react";
9
+ var ELVIX_DEFAULT_BRAND = {
10
+ light: { primary: "#5d4dff", on: "#ffffff" },
11
+ dark: { primary: "#8e7dff", on: "#0a0a0b" }
12
+ };
13
+ var DEFAULT_BASE_URL = "https://elvix.is";
14
+ var BOOTSTRAP_URL = (baseUrl, clientId) => `${baseUrl}/api/v1/bootstrap/${encodeURIComponent(clientId)}`;
15
+ var ElvixContext = createContext(null);
16
+ function useElvixApp() {
17
+ const ctx = useContext(ElvixContext);
18
+ return ctx?.app ?? null;
19
+ }
20
+ function useElvixContext() {
21
+ const ctx = useContext(ElvixContext);
22
+ if (!ctx) {
23
+ throw new Error("Elvix components must be wrapped in <ElvixProvider>.");
24
+ }
25
+ return ctx;
26
+ }
27
+ function ElvixProvider({
28
+ clientId,
29
+ theme,
30
+ brand,
31
+ baseUrl,
32
+ children,
33
+ className = ""
34
+ }) {
35
+ const resolvedBaseUrl = baseUrl ?? DEFAULT_BASE_URL;
36
+ const [app, setApp] = useState(null);
37
+ const [appError, setAppError] = useState(null);
38
+ const [systemDark, setSystemDark] = useState(false);
39
+ useEffect(() => {
40
+ if (!clientId) {
41
+ setApp(null);
42
+ setAppError(null);
43
+ return;
44
+ }
45
+ const ctrl = new AbortController();
46
+ fetch(BOOTSTRAP_URL(resolvedBaseUrl, clientId), { signal: ctrl.signal }).then((r) => r.json()).then((body) => {
47
+ if (body?.success && body?.data) {
48
+ setApp(body.data);
49
+ setAppError(null);
50
+ } else {
51
+ setAppError(body?.errorMessage ?? "bootstrap_failed");
52
+ }
53
+ }).catch((e) => {
54
+ if (e?.name === "AbortError") return;
55
+ setAppError(e instanceof Error ? e.message : "bootstrap_failed");
56
+ });
57
+ return () => ctrl.abort();
58
+ }, [clientId, resolvedBaseUrl]);
59
+ useEffect(() => {
60
+ if (typeof window === "undefined") return;
61
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
62
+ setSystemDark(mq.matches);
63
+ const sync = (e) => setSystemDark(e.matches);
64
+ mq.addEventListener("change", sync);
65
+ return () => mq.removeEventListener("change", sync);
66
+ }, []);
67
+ const effectiveTheme = useMemo(() => {
68
+ if (theme === "light") return "light";
69
+ if (theme === "dark") return "dark";
70
+ return systemDark ? "dark" : "light";
71
+ }, [theme, systemDark]);
72
+ const effectiveBrand = brand ?? appBrand(app) ?? ELVIX_DEFAULT_BRAND;
73
+ const pair = effectiveBrand[effectiveTheme];
74
+ const cssVars = useMemo(
75
+ () => ({
76
+ "--elvix-primary": pair.primary,
77
+ "--elvix-on-primary": pair.on,
78
+ "--elvix-primary-12": withAlpha(pair.primary, 0.12),
79
+ "--elvix-primary-35": withAlpha(pair.primary, 0.35),
80
+ "--elvix-primary-55": withAlpha(pair.primary, 0.55),
81
+ "--elvix-primary-strong": pair.primary
82
+ }),
83
+ [pair.primary, pair.on]
84
+ );
85
+ const value = {
86
+ clientId,
87
+ baseUrl: resolvedBaseUrl,
88
+ app,
89
+ appError,
90
+ resolvedTheme: effectiveTheme
91
+ };
92
+ return /* @__PURE__ */ React.createElement(ElvixContext.Provider, { value }, /* @__PURE__ */ React.createElement(
93
+ "div",
94
+ {
95
+ "data-elvix-theme": effectiveTheme,
96
+ style: cssVars,
97
+ className: (effectiveTheme === "dark" ? "dark " : "") + "elvix-sdk-root " + className
98
+ },
99
+ children
100
+ ));
101
+ }
102
+ function appBrand(app) {
103
+ if (!app?.brand) return null;
104
+ return app.brand;
105
+ }
106
+ function withAlpha(hex, a) {
107
+ const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
108
+ if (!m) return hex;
109
+ const n = Number.parseInt(m[1], 16);
110
+ const r = n >> 16 & 255;
111
+ const g = n >> 8 & 255;
112
+ const b = n & 255;
113
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
114
+ }
115
+
116
+ // src/react/elvix-sign-in.tsx
117
+ import { useState as useState2 } from "react";
118
+ function ElvixSignIn({
119
+ onResult,
120
+ redirectAfterSignIn,
121
+ className = ""
122
+ }) {
123
+ const ctx = useElvixContext();
124
+ const app = useElvixApp();
125
+ const [step, setStep] = useState2("identify");
126
+ const [email, setEmail] = useState2("");
127
+ const [code, setCode] = useState2("");
128
+ const [challengeId, setChallengeId] = useState2(null);
129
+ const [busy, setBusy] = useState2(false);
130
+ const [error, setError] = useState2(null);
131
+ const verb = app?.signInVerb === "login" ? "Log in" : "Sign in";
132
+ function fail(error2, message) {
133
+ setError(message ?? error2);
134
+ onResult?.({ ok: false, error: error2, message });
135
+ }
136
+ async function startGoogle() {
137
+ if (!ctx.clientId) return fail("missing_client_id", "ElvixProvider needs a clientId.");
138
+ window.location.assign(
139
+ `${ctx.baseUrl}/api/auth/google/start?intent=app&clientId=${encodeURIComponent(ctx.clientId)}`
140
+ );
141
+ }
142
+ async function startOtp(e) {
143
+ e.preventDefault();
144
+ if (!email.trim()) return fail("invalid_input", "Enter an email.");
145
+ setBusy(true);
146
+ setError(null);
147
+ try {
148
+ const res = await fetch(`${ctx.baseUrl}/api/auth/otp/start`, {
149
+ method: "POST",
150
+ headers: { "content-type": "application/json" },
151
+ credentials: "include",
152
+ body: JSON.stringify({
153
+ email: email.trim(),
154
+ intent: "app",
155
+ clientId: ctx.clientId
156
+ })
157
+ });
158
+ const body = await res.json();
159
+ if (!res.ok || !body.success || !body.data?.challengeId) {
160
+ return fail(body.errorMessage ?? "otp_start_failed");
161
+ }
162
+ setChallengeId(body.data.challengeId);
163
+ setStep("code");
164
+ } catch (e2) {
165
+ fail("network", e2 instanceof Error ? e2.message : void 0);
166
+ } finally {
167
+ setBusy(false);
168
+ }
169
+ }
170
+ async function verifyOtp(e) {
171
+ e.preventDefault();
172
+ if (!challengeId) return;
173
+ if (code.trim().length !== 6) return fail("invalid_input", "Enter the 6-digit code.");
174
+ setBusy(true);
175
+ setError(null);
176
+ try {
177
+ const res = await fetch(`${ctx.baseUrl}/api/auth/otp/verify`, {
178
+ method: "POST",
179
+ headers: { "content-type": "application/json" },
180
+ credentials: "include",
181
+ body: JSON.stringify({ challengeId, code: code.trim() })
182
+ });
183
+ const body = await res.json();
184
+ if (!res.ok || !body.success) {
185
+ return fail(body.errorMessage ?? "otp_verify_failed");
186
+ }
187
+ setStep("done");
188
+ onResult?.({
189
+ ok: true,
190
+ method: "email_otp",
191
+ redirect: body.data?.redirect ?? redirectAfterSignIn
192
+ });
193
+ } catch (e2) {
194
+ fail("network", e2 instanceof Error ? e2.message : void 0);
195
+ } finally {
196
+ setBusy(false);
197
+ }
198
+ }
199
+ const card = `elvix-card ${className}`.trim();
200
+ if (step === "done") {
201
+ return /* @__PURE__ */ React.createElement("div", { className: card, "data-elvix-pane": "done" }, /* @__PURE__ */ React.createElement("p", null, "Signed in."));
202
+ }
203
+ return /* @__PURE__ */ React.createElement("div", { className: card, "data-elvix-pane": step }, /* @__PURE__ */ React.createElement("h2", { className: "elvix-h" }, app?.appName ? `${verb} to ${app.appName}` : verb), step === "identify" && /* @__PURE__ */ React.createElement(React.Fragment, null, app?.methods.google && /* @__PURE__ */ React.createElement(
204
+ "button",
205
+ {
206
+ type: "button",
207
+ onClick: startGoogle,
208
+ disabled: busy,
209
+ className: "elvix-btn elvix-btn-google",
210
+ "data-elvix-method": "google"
211
+ },
212
+ "Continue with Google"
213
+ ), app?.methods.emailOtp && /* @__PURE__ */ React.createElement("form", { onSubmit: startOtp, "data-elvix-method": "email_otp", className: "elvix-otp-form" }, /* @__PURE__ */ React.createElement(
214
+ "input",
215
+ {
216
+ type: "email",
217
+ value: email,
218
+ onChange: (ev) => setEmail(ev.target.value),
219
+ placeholder: "you@example.com",
220
+ required: true,
221
+ disabled: busy,
222
+ className: "elvix-input"
223
+ }
224
+ ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Sending\u2026" : "Send code"))), step === "code" && /* @__PURE__ */ React.createElement("form", { onSubmit: verifyOtp, className: "elvix-otp-form" }, /* @__PURE__ */ React.createElement("p", { className: "elvix-muted" }, "We sent a 6-digit code to ", /* @__PURE__ */ React.createElement("strong", null, email), "."), /* @__PURE__ */ React.createElement(
225
+ "input",
226
+ {
227
+ type: "text",
228
+ inputMode: "numeric",
229
+ pattern: "[0-9]*",
230
+ maxLength: 6,
231
+ value: code,
232
+ onChange: (ev) => setCode(ev.target.value.replace(/\D/g, "")),
233
+ placeholder: "123456",
234
+ required: true,
235
+ disabled: busy,
236
+ className: "elvix-input"
237
+ }
238
+ ), /* @__PURE__ */ React.createElement("button", { type: "submit", disabled: busy, className: "elvix-btn elvix-btn-primary" }, busy ? "Verifying\u2026" : verb)), error && /* @__PURE__ */ React.createElement("p", { role: "alert", className: "elvix-error" }, error));
239
+ }
240
+ export {
241
+ ElvixProvider,
242
+ ElvixSignIn,
243
+ useElvixApp,
244
+ useElvixContext
245
+ };
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "@elvix.is/sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Official elvix SDK. Drop-in React components, server helpers, and an MCP server so AI coding agents integrate elvix on the first try.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://elvix.is",
7
- "author": "edvone <hi@edvone.dev> (https://edvone.dev)",
7
+ "author": {
8
+ "name": "edvone",
9
+ "url": "https://edvone.dev"
10
+ },
11
+ "funding": "https://edvone.dev/book",
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
8
15
  "repository": {
9
16
  "type": "git",
10
17
  "url": "git+https://github.com/021is/elvix-sdk.git"
@@ -49,13 +56,16 @@
49
56
  "next": ">=15"
50
57
  },
51
58
  "peerDependenciesMeta": {
52
- "next": { "optional": true }
59
+ "next": {
60
+ "optional": true
61
+ }
53
62
  },
54
63
  "dependencies": {
55
64
  "@modelcontextprotocol/sdk": "^1.0.4"
56
65
  },
57
66
  "devDependencies": {
58
67
  "@types/node": "^22.7.5",
68
+ "@types/react": "^19.2.15",
59
69
  "tsup": "^8.3.0",
60
70
  "typescript": "^5.6.3",
61
71
  "vitest": "^2.1.2"