@elvix.is/sdk 0.3.2 → 0.5.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 +15 -1
- package/dist/cli/doctor.d.ts +21 -0
- package/dist/cli/doctor.js +78 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +61 -0
- package/dist/react.d.ts +49 -1
- package/dist/react.js +186 -72
- package/dist/server.d.ts +12 -8
- package/dist/server.js +15 -14
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ elvix ships first-class agent support. Three surfaces:
|
|
|
126
126
|
"mcpServers": {
|
|
127
127
|
"elvix": {
|
|
128
128
|
"command": "npx",
|
|
129
|
-
"args": ["-y", "-p", "@elvix.is/sdk", "elvix
|
|
129
|
+
"args": ["-y", "-p", "@elvix.is/sdk", "elvix", "mcp"],
|
|
130
130
|
"env": { "ELVIX_API_KEY": "eak_..." }
|
|
131
131
|
}
|
|
132
132
|
}
|
|
@@ -135,6 +135,20 @@ elvix ships first-class agent support. Three surfaces:
|
|
|
135
135
|
|
|
136
136
|
Read-only by default. `--admin` opts in to mutation tools. Never logs the bearer token.
|
|
137
137
|
|
|
138
|
+
## CLI
|
|
139
|
+
|
|
140
|
+
The package ships an `elvix` command:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Diagnose an integration — base URL, clientId, verify endpoint, API key.
|
|
144
|
+
ELVIX_CLIENT_ID=client_… npx -p @elvix.is/sdk elvix doctor
|
|
145
|
+
|
|
146
|
+
# Launch the MCP server on stdio.
|
|
147
|
+
ELVIX_API_KEY=eak_… npx -p @elvix.is/sdk elvix mcp
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`elvix doctor` prints a green/red checklist so "why isn't elvix working" is a two-second answer. (`elvix-mcp` is kept as an alias for `elvix mcp`.)
|
|
151
|
+
|
|
138
152
|
Full agent guide: <https://elvix.is/docs/agents>
|
|
139
153
|
|
|
140
154
|
## Components
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `elvix doctor` — diagnose an elvix integration from the terminal.
|
|
3
|
+
*
|
|
4
|
+
* Runs a green/red checklist so a developer (or their agent) can
|
|
5
|
+
* answer "why isn't elvix working" in two seconds instead of a support
|
|
6
|
+
* email. Read-only; never mutates anything.
|
|
7
|
+
*
|
|
8
|
+
* Checks:
|
|
9
|
+
* - base URL reachable (GET /llms.txt)
|
|
10
|
+
* - clientId resolves (GET /api/v1/bootstrap/<clientId> → 200)
|
|
11
|
+
* - verify endpoint live (POST /api/v1/verify with a dummy token → 401)
|
|
12
|
+
* - ELVIX_API_KEY present (informational)
|
|
13
|
+
*
|
|
14
|
+
* Inputs (env or flags):
|
|
15
|
+
* ELVIX_BASE_URL default https://elvix.is (--base-url=)
|
|
16
|
+
* ELVIX_CLIENT_ID the app's public clientId (--client-id=)
|
|
17
|
+
* ELVIX_API_KEY server API key (presence only)
|
|
18
|
+
*/
|
|
19
|
+
declare function runDoctor(): Promise<number>;
|
|
20
|
+
|
|
21
|
+
export { runDoctor };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/cli/doctor.ts
|
|
2
|
+
var DEFAULT_BASE_URL = "https://elvix.is";
|
|
3
|
+
function flag(name) {
|
|
4
|
+
const hit = process.argv.find((a) => a.startsWith(`--${name}=`));
|
|
5
|
+
return hit?.split("=").slice(1).join("=");
|
|
6
|
+
}
|
|
7
|
+
async function timed(fn, timeoutMs = 8e3) {
|
|
8
|
+
const ctrl = new AbortController();
|
|
9
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
10
|
+
try {
|
|
11
|
+
return await fn();
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
} finally {
|
|
15
|
+
clearTimeout(t);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function runDoctor() {
|
|
19
|
+
const baseUrl = flag("base-url") ?? process.env.ELVIX_BASE_URL ?? DEFAULT_BASE_URL;
|
|
20
|
+
const clientId = flag("client-id") ?? process.env.ELVIX_CLIENT_ID;
|
|
21
|
+
const apiKey = process.env.ELVIX_API_KEY;
|
|
22
|
+
const checks = [];
|
|
23
|
+
const llms = await timed(() => fetch(`${baseUrl}/llms.txt`));
|
|
24
|
+
checks.push({
|
|
25
|
+
label: `Base URL reachable (${baseUrl})`,
|
|
26
|
+
ok: Boolean(llms?.ok),
|
|
27
|
+
detail: llms ? `HTTP ${llms.status}` : "no response / timeout"
|
|
28
|
+
});
|
|
29
|
+
if (clientId) {
|
|
30
|
+
const boot = await timed(
|
|
31
|
+
() => fetch(`${baseUrl}/api/v1/bootstrap/${encodeURIComponent(clientId)}`)
|
|
32
|
+
);
|
|
33
|
+
checks.push({
|
|
34
|
+
label: `clientId resolves (${clientId})`,
|
|
35
|
+
ok: Boolean(boot?.ok),
|
|
36
|
+
detail: boot ? boot.ok ? `HTTP ${boot.status}` : `HTTP ${boot.status} \u2014 wrong clientId, or app is draft/deleted` : "no response / timeout"
|
|
37
|
+
});
|
|
38
|
+
} else {
|
|
39
|
+
checks.push({
|
|
40
|
+
label: "clientId resolves",
|
|
41
|
+
ok: false,
|
|
42
|
+
detail: "skipped \u2014 pass --client-id=<id> or set ELVIX_CLIENT_ID"
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const verify = await timed(
|
|
46
|
+
() => fetch(`${baseUrl}/api/v1/verify`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "content-type": "application/json" },
|
|
49
|
+
body: JSON.stringify({ token: "doctor-probe" })
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
checks.push({
|
|
53
|
+
label: "Verify endpoint live (POST /api/v1/verify)",
|
|
54
|
+
ok: verify?.status === 401,
|
|
55
|
+
detail: verify ? `HTTP ${verify.status} (401 expected)` : "no response / timeout"
|
|
56
|
+
});
|
|
57
|
+
checks.push({
|
|
58
|
+
label: "ELVIX_API_KEY present",
|
|
59
|
+
ok: Boolean(apiKey),
|
|
60
|
+
detail: apiKey ? `set (${apiKey.slice(0, 8)}\u2026)` : "not set \u2014 server-side verify will fail"
|
|
61
|
+
});
|
|
62
|
+
let criticalFail = false;
|
|
63
|
+
process.stdout.write("\nelvix doctor\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
64
|
+
for (const c of checks) {
|
|
65
|
+
const mark = c.ok ? "\u2713" : "\u2717";
|
|
66
|
+
process.stdout.write(` ${mark} ${c.label}
|
|
67
|
+
${c.detail}
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
if (!checks[0]?.ok || !checks[2]?.ok) criticalFail = true;
|
|
71
|
+
process.stdout.write(
|
|
72
|
+
criticalFail ? "\nResult: elvix is NOT reachable from here. Check network / base URL.\n" : "\nResult: elvix is reachable. Warnings above (if any) are config hints.\n"
|
|
73
|
+
);
|
|
74
|
+
return criticalFail ? 1 : 0;
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
runDoctor
|
|
78
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
var HELP = `elvix \u2014 @elvix.is/sdk CLI
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
elvix mcp [--admin] [--base-url=<url>]
|
|
8
|
+
Launch the elvix MCP server on stdio. Reads ELVIX_API_KEY from
|
|
9
|
+
the environment. Read-only by default; --admin enables mutation
|
|
10
|
+
tools (the server still enforces the admin scope on the key).
|
|
11
|
+
|
|
12
|
+
elvix doctor [--client-id=<id>] [--base-url=<url>]
|
|
13
|
+
Diagnose an integration: base URL reachability, clientId
|
|
14
|
+
resolution, verify-endpoint liveness, API-key presence.
|
|
15
|
+
Reads ELVIX_CLIENT_ID / ELVIX_API_KEY / ELVIX_BASE_URL.
|
|
16
|
+
|
|
17
|
+
elvix help
|
|
18
|
+
Show this message.
|
|
19
|
+
|
|
20
|
+
Docs: https://elvix.is/docs/agents`;
|
|
21
|
+
async function main() {
|
|
22
|
+
const sub = process.argv[2];
|
|
23
|
+
switch (sub) {
|
|
24
|
+
case "mcp": {
|
|
25
|
+
const { createElvixMcpServer } = await import("../mcp/index.js");
|
|
26
|
+
const apiKey = process.env.ELVIX_API_KEY;
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
process.stderr.write("ELVIX_API_KEY environment variable is required.\n");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const admin = process.argv.includes("--admin");
|
|
32
|
+
const baseUrl = process.argv.find((a) => a.startsWith("--base-url="))?.split("=")[1];
|
|
33
|
+
const { connectStdio } = await createElvixMcpServer({ apiKey, readonly: !admin, baseUrl });
|
|
34
|
+
await connectStdio();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
case "doctor": {
|
|
38
|
+
const { runDoctor } = await import("./doctor.js");
|
|
39
|
+
process.exit(await runDoctor());
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case "help":
|
|
43
|
+
case "--help":
|
|
44
|
+
case "-h":
|
|
45
|
+
case void 0:
|
|
46
|
+
process.stdout.write(`${HELP}
|
|
47
|
+
`);
|
|
48
|
+
return;
|
|
49
|
+
default:
|
|
50
|
+
process.stderr.write(`Unknown command: ${sub}
|
|
51
|
+
|
|
52
|
+
${HELP}
|
|
53
|
+
`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
main().catch((e) => {
|
|
58
|
+
process.stderr.write(`elvix: ${e instanceof Error ? e.message : String(e)}
|
|
59
|
+
`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
package/dist/react.d.ts
CHANGED
|
@@ -67,6 +67,12 @@ type ElvixSignInResultOk = {
|
|
|
67
67
|
redirect?: string;
|
|
68
68
|
/** Sign-in factor that succeeded. */
|
|
69
69
|
method: ElvixSignInMethod;
|
|
70
|
+
/**
|
|
71
|
+
* Cross-origin only: the session token. Pass it to your backend and verify
|
|
72
|
+
* it with `verifyElvixToken` from `@elvix.is/sdk/server`. Undefined for
|
|
73
|
+
* same-origin sign-in (the session rides a cookie instead).
|
|
74
|
+
*/
|
|
75
|
+
token?: string;
|
|
70
76
|
};
|
|
71
77
|
type ElvixSignInResultErr = {
|
|
72
78
|
ok: false;
|
|
@@ -120,6 +126,48 @@ declare function ElvixSignIn({ onResult, redirectAfterSignIn, className, }: {
|
|
|
120
126
|
className?: string;
|
|
121
127
|
}): react.JSX.Element;
|
|
122
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Session token store for cross-origin SDK use.
|
|
131
|
+
*
|
|
132
|
+
* When the SDK runs on a customer app's own origin it can't use elvix's
|
|
133
|
+
* session cookie (third-party cookies are blocked), so sign-in returns a
|
|
134
|
+
* token that every subsequent call carries as `Authorization: Bearer`.
|
|
135
|
+
* `<ElvixSignIn>` stores it here; `appPost/appPatch/appDelete` and the live
|
|
136
|
+
* hooks attach it. Same-origin hosts never store a token, so these are no-ops
|
|
137
|
+
* and the cookie path is used unchanged.
|
|
138
|
+
*/
|
|
139
|
+
/** Current session token (memory first, then localStorage), or null. */
|
|
140
|
+
declare function getElvixToken(): string | null;
|
|
141
|
+
/** Store (or clear with null) the session token. Persists to localStorage. */
|
|
142
|
+
declare function setElvixToken(token: string | null): void;
|
|
143
|
+
|
|
144
|
+
type UseUserListResult = {
|
|
145
|
+
slugs: string[];
|
|
146
|
+
loading: boolean;
|
|
147
|
+
error: string | null;
|
|
148
|
+
refresh: () => Promise<void>;
|
|
149
|
+
};
|
|
150
|
+
type Opts = {
|
|
151
|
+
applicationId: string;
|
|
152
|
+
userId: string;
|
|
153
|
+
/** elvix origin. Defaults to "" (same-origin). */
|
|
154
|
+
baseUrl?: string;
|
|
155
|
+
/** Poll interval in ms. Default 7000. */
|
|
156
|
+
pollMs?: number;
|
|
157
|
+
};
|
|
158
|
+
declare const useUserRoles: (opts: Opts) => UseUserListResult;
|
|
159
|
+
declare const useUserScopes: (opts: Opts) => UseUserListResult;
|
|
160
|
+
declare const useUserMemberships: (opts: Opts) => UseUserListResult;
|
|
161
|
+
|
|
162
|
+
declare function ElvixLifecycleWatcher({ baseUrl, pollMs, onSignedOut, }: {
|
|
163
|
+
/** elvix origin. Defaults to "" (same-origin). */
|
|
164
|
+
baseUrl?: string;
|
|
165
|
+
/** Poll interval in ms. Default 7000. */
|
|
166
|
+
pollMs?: number;
|
|
167
|
+
/** Called once with the reason when the session ends. Defaults to a reload. */
|
|
168
|
+
onSignedOut?: (reason: string) => void;
|
|
169
|
+
}): null;
|
|
170
|
+
|
|
123
171
|
/**
|
|
124
172
|
* `<ElvixUsername>` — claim or change the end-user's username for the
|
|
125
173
|
* current Application. PATCH /api/account/apps/<appId>/username.
|
|
@@ -232,4 +280,4 @@ declare function ElvixLegalEntities({ onResult, }: {
|
|
|
232
280
|
onResult?: (r: ElvixActionResult) => void;
|
|
233
281
|
}): react.JSX.Element;
|
|
234
282
|
|
|
235
|
-
export { ElvixActionResult, ElvixAddressBook, ElvixAvatar, ElvixBanner, type ElvixBootstrapEnvelope, type ElvixBrand, ElvixCard, ElvixDeactivate, ElvixExport, ElvixIdentityForm, ElvixLanguages, ElvixLeave, ElvixLegalEntities, ElvixProvider, ElvixRegion, ElvixSessions, ElvixSignIn, type ElvixSignInMethod, type ElvixSignInResult, type ElvixSignInResultErr, type ElvixSignInResultOk, type ElvixTheme, ElvixUsername, useElvixApp, useElvixContext };
|
|
283
|
+
export { ElvixActionResult, ElvixAddressBook, ElvixAvatar, ElvixBanner, type ElvixBootstrapEnvelope, type ElvixBrand, ElvixCard, ElvixDeactivate, ElvixExport, ElvixIdentityForm, ElvixLanguages, ElvixLeave, ElvixLegalEntities, ElvixLifecycleWatcher, ElvixProvider, ElvixRegion, ElvixSessions, ElvixSignIn, type ElvixSignInMethod, type ElvixSignInResult, type ElvixSignInResultErr, type ElvixSignInResultOk, type ElvixTheme, ElvixUsername, type UseUserListResult, getElvixToken, setElvixToken, useElvixApp, useElvixContext, useUserMemberships, useUserRoles, useUserScopes };
|
package/dist/react.js
CHANGED
|
@@ -166,6 +166,35 @@ function withAlpha(hex, a) {
|
|
|
166
166
|
|
|
167
167
|
// src/react/elvix-sign-in.tsx
|
|
168
168
|
import { useState as useState2 } from "react";
|
|
169
|
+
|
|
170
|
+
// src/react/session.ts
|
|
171
|
+
var STORAGE_KEY = "elvix.session.token";
|
|
172
|
+
var memToken = null;
|
|
173
|
+
function getElvixToken() {
|
|
174
|
+
if (memToken) return memToken;
|
|
175
|
+
if (typeof window !== "undefined") {
|
|
176
|
+
try {
|
|
177
|
+
memToken = window.localStorage.getItem(STORAGE_KEY);
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return memToken;
|
|
182
|
+
}
|
|
183
|
+
function setElvixToken(token) {
|
|
184
|
+
memToken = token;
|
|
185
|
+
if (typeof window === "undefined") return;
|
|
186
|
+
try {
|
|
187
|
+
if (token) window.localStorage.setItem(STORAGE_KEY, token);
|
|
188
|
+
else window.localStorage.removeItem(STORAGE_KEY);
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function authInit() {
|
|
193
|
+
const token = getElvixToken();
|
|
194
|
+
return token ? { headers: { authorization: `Bearer ${token}` }, credentials: "omit" } : { headers: {}, credentials: "include" };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/react/elvix-sign-in.tsx
|
|
169
198
|
function ElvixSignIn({
|
|
170
199
|
onResult,
|
|
171
200
|
redirectAfterSignIn,
|
|
@@ -235,11 +264,13 @@ function ElvixSignIn({
|
|
|
235
264
|
if (!res.ok || !body.success) {
|
|
236
265
|
return fail(body.errorMessage ?? "otp_verify_failed");
|
|
237
266
|
}
|
|
267
|
+
if (body.data?.token) setElvixToken(body.data.token);
|
|
238
268
|
setStep("done");
|
|
239
269
|
onResult?.({
|
|
240
270
|
ok: true,
|
|
241
271
|
method: "email_otp",
|
|
242
|
-
redirect: body.data?.redirect ?? redirectAfterSignIn
|
|
272
|
+
redirect: body.data?.redirect ?? redirectAfterSignIn,
|
|
273
|
+
token: body.data?.token
|
|
243
274
|
});
|
|
244
275
|
} catch (e2) {
|
|
245
276
|
fail("network", e2 instanceof Error ? e2.message : void 0);
|
|
@@ -289,16 +320,90 @@ function ElvixSignIn({
|
|
|
289
320
|
), /* @__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));
|
|
290
321
|
}
|
|
291
322
|
|
|
323
|
+
// src/react/hooks.ts
|
|
324
|
+
import { useCallback, useEffect as useEffect2, useState as useState3 } from "react";
|
|
325
|
+
var POLL_MS = 7e3;
|
|
326
|
+
function useUserList(kind, opts) {
|
|
327
|
+
const { applicationId, baseUrl = "", pollMs = POLL_MS } = opts;
|
|
328
|
+
const [slugs, setSlugs] = useState3([]);
|
|
329
|
+
const [loading, setLoading] = useState3(true);
|
|
330
|
+
const [error, setError] = useState3(null);
|
|
331
|
+
const refresh = useCallback(async () => {
|
|
332
|
+
setError(null);
|
|
333
|
+
try {
|
|
334
|
+
const res = await fetch(
|
|
335
|
+
`${baseUrl}/api/me/${kind}?applicationId=${encodeURIComponent(applicationId)}`,
|
|
336
|
+
authInit()
|
|
337
|
+
);
|
|
338
|
+
const json = await res.json().catch(() => ({}));
|
|
339
|
+
if (!res.ok || json.success === false) {
|
|
340
|
+
setError(json.errorMessage ?? `http_${res.status}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
setSlugs(json.data?.slugs ?? []);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
setError(e instanceof Error ? e.message : "network");
|
|
346
|
+
} finally {
|
|
347
|
+
setLoading(false);
|
|
348
|
+
}
|
|
349
|
+
}, [applicationId, baseUrl, kind]);
|
|
350
|
+
useEffect2(() => {
|
|
351
|
+
refresh();
|
|
352
|
+
const id = setInterval(refresh, pollMs);
|
|
353
|
+
return () => clearInterval(id);
|
|
354
|
+
}, [refresh, pollMs]);
|
|
355
|
+
return { slugs, loading, error, refresh };
|
|
356
|
+
}
|
|
357
|
+
var useUserRoles = (opts) => useUserList("roles", opts);
|
|
358
|
+
var useUserScopes = (opts) => useUserList("scopes", opts);
|
|
359
|
+
var useUserMemberships = (opts) => useUserList("memberships", opts);
|
|
360
|
+
|
|
361
|
+
// src/react/lifecycle-watcher.tsx
|
|
362
|
+
import { useEffect as useEffect3 } from "react";
|
|
363
|
+
function ElvixLifecycleWatcher({
|
|
364
|
+
baseUrl = "",
|
|
365
|
+
pollMs = 7e3,
|
|
366
|
+
onSignedOut
|
|
367
|
+
}) {
|
|
368
|
+
useEffect3(() => {
|
|
369
|
+
let cancelled = false;
|
|
370
|
+
let fired = false;
|
|
371
|
+
const poll = async () => {
|
|
372
|
+
try {
|
|
373
|
+
const res = await fetch(`${baseUrl}/api/v1/session`, { method: "POST", ...authInit() });
|
|
374
|
+
const body = await res.json().catch(() => ({}));
|
|
375
|
+
if (cancelled || fired) return;
|
|
376
|
+
if (!body.ok) {
|
|
377
|
+
fired = true;
|
|
378
|
+
setElvixToken(null);
|
|
379
|
+
const reason = body.error ?? "signed_out";
|
|
380
|
+
if (onSignedOut) onSignedOut(reason);
|
|
381
|
+
else if (typeof window !== "undefined") window.location.reload();
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
void poll();
|
|
387
|
+
const id = setInterval(poll, pollMs);
|
|
388
|
+
return () => {
|
|
389
|
+
cancelled = true;
|
|
390
|
+
clearInterval(id);
|
|
391
|
+
};
|
|
392
|
+
}, [baseUrl, pollMs, onSignedOut]);
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
292
396
|
// src/react/elvix-username.tsx
|
|
293
|
-
import { useState as
|
|
397
|
+
import { useState as useState4 } from "react";
|
|
294
398
|
|
|
295
399
|
// src/react/lib.ts
|
|
296
400
|
async function appPost(opts, path, body) {
|
|
297
401
|
try {
|
|
402
|
+
const auth = authInit();
|
|
298
403
|
const res = await fetch(`${opts.baseUrl}/api/account/apps/${opts.applicationId}${path}`, {
|
|
299
404
|
method: "POST",
|
|
300
|
-
headers: { "content-type": "application/json" },
|
|
301
|
-
credentials:
|
|
405
|
+
headers: { "content-type": "application/json", ...auth.headers },
|
|
406
|
+
credentials: auth.credentials,
|
|
302
407
|
body: JSON.stringify(body)
|
|
303
408
|
});
|
|
304
409
|
const json = await res.json();
|
|
@@ -312,10 +417,11 @@ async function appPost(opts, path, body) {
|
|
|
312
417
|
}
|
|
313
418
|
async function appPatch(opts, path, body) {
|
|
314
419
|
try {
|
|
420
|
+
const auth = authInit();
|
|
315
421
|
const res = await fetch(`${opts.baseUrl}/api/account/apps/${opts.applicationId}${path}`, {
|
|
316
422
|
method: "PATCH",
|
|
317
|
-
headers: { "content-type": "application/json" },
|
|
318
|
-
credentials:
|
|
423
|
+
headers: { "content-type": "application/json", ...auth.headers },
|
|
424
|
+
credentials: auth.credentials,
|
|
319
425
|
body: JSON.stringify(body)
|
|
320
426
|
});
|
|
321
427
|
const json = await res.json();
|
|
@@ -329,9 +435,11 @@ async function appPatch(opts, path, body) {
|
|
|
329
435
|
}
|
|
330
436
|
async function appDelete(opts, path) {
|
|
331
437
|
try {
|
|
438
|
+
const auth = authInit();
|
|
332
439
|
const res = await fetch(`${opts.baseUrl}/api/account/apps/${opts.applicationId}${path}`, {
|
|
333
440
|
method: "DELETE",
|
|
334
|
-
|
|
441
|
+
headers: { ...auth.headers },
|
|
442
|
+
credentials: auth.credentials
|
|
335
443
|
});
|
|
336
444
|
const json = await res.json().catch(() => ({}));
|
|
337
445
|
if (!res.ok || json.success !== void 0 && !json.success) {
|
|
@@ -348,10 +456,10 @@ function ElvixUsername({
|
|
|
348
456
|
onResult
|
|
349
457
|
}) {
|
|
350
458
|
const ctx = useElvixContext();
|
|
351
|
-
const [value, setValue] =
|
|
352
|
-
const [busy, setBusy] =
|
|
353
|
-
const [error, setError] =
|
|
354
|
-
const [done, setDone] =
|
|
459
|
+
const [value, setValue] = useState4("");
|
|
460
|
+
const [busy, setBusy] = useState4(false);
|
|
461
|
+
const [error, setError] = useState4(null);
|
|
462
|
+
const [done, setDone] = useState4(null);
|
|
355
463
|
async function submit(e) {
|
|
356
464
|
e.preventDefault();
|
|
357
465
|
if (!ctx.app) return;
|
|
@@ -389,14 +497,14 @@ function ElvixUsername({
|
|
|
389
497
|
}
|
|
390
498
|
|
|
391
499
|
// src/react/elvix-avatar.tsx
|
|
392
|
-
import { useState as
|
|
500
|
+
import { useState as useState5 } from "react";
|
|
393
501
|
function ElvixAvatar({
|
|
394
502
|
onResult
|
|
395
503
|
}) {
|
|
396
504
|
const ctx = useElvixContext();
|
|
397
|
-
const [busy, setBusy] =
|
|
398
|
-
const [error, setError] =
|
|
399
|
-
const [preview, setPreview] =
|
|
505
|
+
const [busy, setBusy] = useState5(false);
|
|
506
|
+
const [error, setError] = useState5(null);
|
|
507
|
+
const [preview, setPreview] = useState5(null);
|
|
400
508
|
async function onFile(e) {
|
|
401
509
|
const file = e.target.files?.[0];
|
|
402
510
|
if (!file || !ctx.app) return;
|
|
@@ -430,14 +538,14 @@ function ElvixAvatar({
|
|
|
430
538
|
}
|
|
431
539
|
|
|
432
540
|
// src/react/elvix-banner.tsx
|
|
433
|
-
import { useState as
|
|
541
|
+
import { useState as useState6 } from "react";
|
|
434
542
|
function ElvixBanner({
|
|
435
543
|
onResult
|
|
436
544
|
}) {
|
|
437
545
|
const ctx = useElvixContext();
|
|
438
|
-
const [busy, setBusy] =
|
|
439
|
-
const [error, setError] =
|
|
440
|
-
const [preview, setPreview] =
|
|
546
|
+
const [busy, setBusy] = useState6(false);
|
|
547
|
+
const [error, setError] = useState6(null);
|
|
548
|
+
const [preview, setPreview] = useState6(null);
|
|
441
549
|
async function onFile(e) {
|
|
442
550
|
const file = e.target.files?.[0];
|
|
443
551
|
if (!file || !ctx.app) return;
|
|
@@ -471,18 +579,18 @@ function ElvixBanner({
|
|
|
471
579
|
}
|
|
472
580
|
|
|
473
581
|
// src/react/elvix-identity-form.tsx
|
|
474
|
-
import { useState as
|
|
582
|
+
import { useState as useState7 } from "react";
|
|
475
583
|
function ElvixIdentityForm({
|
|
476
584
|
initialName = "",
|
|
477
585
|
initialBio = "",
|
|
478
586
|
onResult
|
|
479
587
|
}) {
|
|
480
588
|
const ctx = useElvixContext();
|
|
481
|
-
const [name, setName] =
|
|
482
|
-
const [bio, setBio] =
|
|
483
|
-
const [busy, setBusy] =
|
|
484
|
-
const [error, setError] =
|
|
485
|
-
const [saved, setSaved] =
|
|
589
|
+
const [name, setName] = useState7(initialName);
|
|
590
|
+
const [bio, setBio] = useState7(initialBio);
|
|
591
|
+
const [busy, setBusy] = useState7(false);
|
|
592
|
+
const [error, setError] = useState7(null);
|
|
593
|
+
const [saved, setSaved] = useState7(false);
|
|
486
594
|
async function submit(e) {
|
|
487
595
|
e.preventDefault();
|
|
488
596
|
if (!ctx.app) return;
|
|
@@ -502,18 +610,18 @@ function ElvixIdentityForm({
|
|
|
502
610
|
}
|
|
503
611
|
|
|
504
612
|
// src/react/elvix-region.tsx
|
|
505
|
-
import { useState as
|
|
613
|
+
import { useState as useState8 } from "react";
|
|
506
614
|
function ElvixRegion({
|
|
507
615
|
initialCountry = "",
|
|
508
616
|
initialTimezone = "",
|
|
509
617
|
onResult
|
|
510
618
|
}) {
|
|
511
619
|
const ctx = useElvixContext();
|
|
512
|
-
const [country, setCountry] =
|
|
513
|
-
const [timezone, setTimezone] =
|
|
514
|
-
const [busy, setBusy] =
|
|
515
|
-
const [error, setError] =
|
|
516
|
-
const [saved, setSaved] =
|
|
620
|
+
const [country, setCountry] = useState8(initialCountry);
|
|
621
|
+
const [timezone, setTimezone] = useState8(initialTimezone);
|
|
622
|
+
const [busy, setBusy] = useState8(false);
|
|
623
|
+
const [error, setError] = useState8(null);
|
|
624
|
+
const [saved, setSaved] = useState8(false);
|
|
517
625
|
async function submit(e) {
|
|
518
626
|
e.preventDefault();
|
|
519
627
|
if (!ctx.app) return;
|
|
@@ -533,16 +641,16 @@ function ElvixRegion({
|
|
|
533
641
|
}
|
|
534
642
|
|
|
535
643
|
// src/react/elvix-languages.tsx
|
|
536
|
-
import { useState as
|
|
644
|
+
import { useState as useState9 } from "react";
|
|
537
645
|
function ElvixLanguages({
|
|
538
646
|
initial = [],
|
|
539
647
|
onResult
|
|
540
648
|
}) {
|
|
541
649
|
const ctx = useElvixContext();
|
|
542
|
-
const [raw, setRaw] =
|
|
543
|
-
const [busy, setBusy] =
|
|
544
|
-
const [error, setError] =
|
|
545
|
-
const [saved, setSaved] =
|
|
650
|
+
const [raw, setRaw] = useState9(initial.join(", "));
|
|
651
|
+
const [busy, setBusy] = useState9(false);
|
|
652
|
+
const [error, setError] = useState9(null);
|
|
653
|
+
const [saved, setSaved] = useState9(false);
|
|
546
654
|
async function submit(e) {
|
|
547
655
|
e.preventDefault();
|
|
548
656
|
if (!ctx.app) return;
|
|
@@ -563,15 +671,15 @@ function ElvixLanguages({
|
|
|
563
671
|
}
|
|
564
672
|
|
|
565
673
|
// src/react/elvix-sessions.tsx
|
|
566
|
-
import { useEffect as
|
|
674
|
+
import { useEffect as useEffect4, useState as useState10 } from "react";
|
|
567
675
|
function ElvixSessions({
|
|
568
676
|
onResult
|
|
569
677
|
}) {
|
|
570
678
|
const ctx = useElvixContext();
|
|
571
|
-
const [rows, setRows] =
|
|
572
|
-
const [error, setError] =
|
|
573
|
-
const [busy, setBusy] =
|
|
574
|
-
|
|
679
|
+
const [rows, setRows] = useState10(null);
|
|
680
|
+
const [error, setError] = useState10(null);
|
|
681
|
+
const [busy, setBusy] = useState10(false);
|
|
682
|
+
useEffect4(() => {
|
|
575
683
|
if (!ctx.app) return;
|
|
576
684
|
fetch(`${ctx.baseUrl}/api/account/apps/${ctx.app.applicationId}/sessions`, {
|
|
577
685
|
credentials: "include"
|
|
@@ -595,14 +703,14 @@ function ElvixSessions({
|
|
|
595
703
|
}
|
|
596
704
|
|
|
597
705
|
// src/react/elvix-export.tsx
|
|
598
|
-
import { useState as
|
|
706
|
+
import { useState as useState11 } from "react";
|
|
599
707
|
function ElvixExport({
|
|
600
708
|
onResult
|
|
601
709
|
}) {
|
|
602
710
|
const ctx = useElvixContext();
|
|
603
|
-
const [busy, setBusy] =
|
|
604
|
-
const [done, setDone] =
|
|
605
|
-
const [error, setError] =
|
|
711
|
+
const [busy, setBusy] = useState11(false);
|
|
712
|
+
const [done, setDone] = useState11(false);
|
|
713
|
+
const [error, setError] = useState11(null);
|
|
606
714
|
async function start() {
|
|
607
715
|
if (!ctx.app) return;
|
|
608
716
|
setBusy(true);
|
|
@@ -621,16 +729,16 @@ function ElvixExport({
|
|
|
621
729
|
}
|
|
622
730
|
|
|
623
731
|
// src/react/elvix-deactivate.tsx
|
|
624
|
-
import { useState as
|
|
732
|
+
import { useState as useState12 } from "react";
|
|
625
733
|
function ElvixDeactivate({
|
|
626
734
|
onResult
|
|
627
735
|
}) {
|
|
628
736
|
const ctx = useElvixContext();
|
|
629
|
-
const [pane, setPane] =
|
|
630
|
-
const [challengeId, setChallengeId] =
|
|
631
|
-
const [code, setCode] =
|
|
632
|
-
const [busy, setBusy] =
|
|
633
|
-
const [error, setError] =
|
|
737
|
+
const [pane, setPane] = useState12("warn");
|
|
738
|
+
const [challengeId, setChallengeId] = useState12(null);
|
|
739
|
+
const [code, setCode] = useState12("");
|
|
740
|
+
const [busy, setBusy] = useState12(false);
|
|
741
|
+
const [error, setError] = useState12(null);
|
|
634
742
|
async function startChallenge() {
|
|
635
743
|
if (!ctx.app) return;
|
|
636
744
|
setBusy(true);
|
|
@@ -687,16 +795,16 @@ function ElvixDeactivate({
|
|
|
687
795
|
}
|
|
688
796
|
|
|
689
797
|
// src/react/elvix-leave.tsx
|
|
690
|
-
import { useState as
|
|
798
|
+
import { useState as useState13 } from "react";
|
|
691
799
|
function ElvixLeave({
|
|
692
800
|
onResult
|
|
693
801
|
}) {
|
|
694
802
|
const ctx = useElvixContext();
|
|
695
|
-
const [pane, setPane] =
|
|
696
|
-
const [challengeId, setChallengeId] =
|
|
697
|
-
const [code, setCode] =
|
|
698
|
-
const [busy, setBusy] =
|
|
699
|
-
const [error, setError] =
|
|
803
|
+
const [pane, setPane] = useState13("warn");
|
|
804
|
+
const [challengeId, setChallengeId] = useState13(null);
|
|
805
|
+
const [code, setCode] = useState13("");
|
|
806
|
+
const [busy, setBusy] = useState13(false);
|
|
807
|
+
const [error, setError] = useState13(null);
|
|
700
808
|
async function startChallenge() {
|
|
701
809
|
if (!ctx.app) return;
|
|
702
810
|
setBusy(true);
|
|
@@ -753,16 +861,16 @@ function ElvixLeave({
|
|
|
753
861
|
}
|
|
754
862
|
|
|
755
863
|
// src/react/elvix-address-book.tsx
|
|
756
|
-
import { useEffect as
|
|
864
|
+
import { useEffect as useEffect5, useState as useState14 } from "react";
|
|
757
865
|
function ElvixAddressBook({
|
|
758
866
|
onResult
|
|
759
867
|
}) {
|
|
760
868
|
const ctx = useElvixContext();
|
|
761
|
-
const [rows, setRows] =
|
|
762
|
-
const [error, setError] =
|
|
763
|
-
const [busy, setBusy] =
|
|
764
|
-
const [adding, setAdding] =
|
|
765
|
-
const [form, setForm] =
|
|
869
|
+
const [rows, setRows] = useState14(null);
|
|
870
|
+
const [error, setError] = useState14(null);
|
|
871
|
+
const [busy, setBusy] = useState14(false);
|
|
872
|
+
const [adding, setAdding] = useState14(false);
|
|
873
|
+
const [form, setForm] = useState14({
|
|
766
874
|
label: "Home",
|
|
767
875
|
line1: "",
|
|
768
876
|
postalCode: "",
|
|
@@ -778,7 +886,7 @@ function ElvixAddressBook({
|
|
|
778
886
|
else setError("load_failed");
|
|
779
887
|
}).catch(() => setError("network"));
|
|
780
888
|
}
|
|
781
|
-
|
|
889
|
+
useEffect5(() => {
|
|
782
890
|
reload();
|
|
783
891
|
}, [ctx.app, ctx.baseUrl]);
|
|
784
892
|
async function add(e) {
|
|
@@ -815,16 +923,16 @@ function ElvixAddressBook({
|
|
|
815
923
|
}
|
|
816
924
|
|
|
817
925
|
// src/react/elvix-legal-entities.tsx
|
|
818
|
-
import { useEffect as
|
|
926
|
+
import { useEffect as useEffect6, useState as useState15 } from "react";
|
|
819
927
|
function ElvixLegalEntities({
|
|
820
928
|
onResult
|
|
821
929
|
}) {
|
|
822
930
|
const ctx = useElvixContext();
|
|
823
|
-
const [rows, setRows] =
|
|
824
|
-
const [error, setError] =
|
|
825
|
-
const [busy, setBusy] =
|
|
826
|
-
const [adding, setAdding] =
|
|
827
|
-
const [form, setForm] =
|
|
931
|
+
const [rows, setRows] = useState15(null);
|
|
932
|
+
const [error, setError] = useState15(null);
|
|
933
|
+
const [busy, setBusy] = useState15(false);
|
|
934
|
+
const [adding, setAdding] = useState15(false);
|
|
935
|
+
const [form, setForm] = useState15({
|
|
828
936
|
legalName: "",
|
|
829
937
|
taxId: "",
|
|
830
938
|
country: ""
|
|
@@ -838,7 +946,7 @@ function ElvixLegalEntities({
|
|
|
838
946
|
else setError("load_failed");
|
|
839
947
|
}).catch(() => setError("network"));
|
|
840
948
|
}
|
|
841
|
-
|
|
949
|
+
useEffect6(() => {
|
|
842
950
|
reload();
|
|
843
951
|
}, [ctx.app, ctx.baseUrl]);
|
|
844
952
|
async function add(e) {
|
|
@@ -884,11 +992,17 @@ export {
|
|
|
884
992
|
ElvixLanguages,
|
|
885
993
|
ElvixLeave,
|
|
886
994
|
ElvixLegalEntities,
|
|
995
|
+
ElvixLifecycleWatcher,
|
|
887
996
|
ElvixProvider,
|
|
888
997
|
ElvixRegion,
|
|
889
998
|
ElvixSessions,
|
|
890
999
|
ElvixSignIn,
|
|
891
1000
|
ElvixUsername,
|
|
1001
|
+
getElvixToken,
|
|
1002
|
+
setElvixToken,
|
|
892
1003
|
useElvixApp,
|
|
893
|
-
useElvixContext
|
|
1004
|
+
useElvixContext,
|
|
1005
|
+
useUserMemberships,
|
|
1006
|
+
useUserRoles,
|
|
1007
|
+
useUserScopes
|
|
894
1008
|
};
|
package/dist/server.d.ts
CHANGED
|
@@ -7,21 +7,25 @@ import { ElvixVerifyResult } from './index.js';
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
type VerifyOptions = {
|
|
10
|
-
/** Application API key (Console → Credentials). */
|
|
11
|
-
apiKey: string;
|
|
12
10
|
/** Override the elvix origin for testing / proxy setups. */
|
|
13
11
|
baseUrl?: string;
|
|
14
12
|
/** Per-request timeout in ms. Default 5000. */
|
|
15
13
|
timeoutMs?: number;
|
|
16
14
|
};
|
|
17
15
|
/**
|
|
18
|
-
*
|
|
19
|
-
* (
|
|
20
|
-
*
|
|
16
|
+
* Verify an end-user session token (the value the SDK handed you via
|
|
17
|
+
* `onSuccess({ token })`) and get back the live user envelope — roles,
|
|
18
|
+
* scopes, memberships — for the token's application.
|
|
21
19
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* The token is self-authenticating: POST it as a Bearer to
|
|
21
|
+
* `/api/v1/session`. elvix re-checks the session and the user/membership
|
|
22
|
+
* status on every call, so a banned, paused, or signed-out user verifies as
|
|
23
|
+
* `ok:false` here within one request — call this on each protected request
|
|
24
|
+
* (or cache for a few seconds) and you enforce bans server-side too.
|
|
25
|
+
*
|
|
26
|
+
* Returns a discriminated union — never throws on auth failure. Throws only
|
|
27
|
+
* on infra failure (network, timeout, malformed JSON).
|
|
24
28
|
*/
|
|
25
|
-
declare function verifyElvixToken(token: string, opts
|
|
29
|
+
declare function verifyElvixToken(token: string, opts?: VerifyOptions): Promise<ElvixVerifyResult>;
|
|
26
30
|
|
|
27
31
|
export { type VerifyOptions, verifyElvixToken };
|
package/dist/server.js
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
// src/server.ts
|
|
2
2
|
var DEFAULT_BASE_URL = "https://elvix.is";
|
|
3
|
-
async function verifyElvixToken(token, opts) {
|
|
4
|
-
const url = `${opts.baseUrl ?? DEFAULT_BASE_URL}/api/v1/
|
|
3
|
+
async function verifyElvixToken(token, opts = {}) {
|
|
4
|
+
const url = `${opts.baseUrl ?? DEFAULT_BASE_URL}/api/v1/session`;
|
|
5
5
|
const ctrl = new AbortController();
|
|
6
6
|
const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 5e3);
|
|
7
7
|
try {
|
|
8
8
|
const res = await fetch(url, {
|
|
9
9
|
method: "POST",
|
|
10
|
-
headers: {
|
|
11
|
-
"content-type": "application/json",
|
|
12
|
-
authorization: `Bearer ${opts.apiKey}`
|
|
13
|
-
},
|
|
14
|
-
body: JSON.stringify({ token }),
|
|
10
|
+
headers: { authorization: `Bearer ${token}` },
|
|
15
11
|
signal: ctrl.signal
|
|
16
12
|
});
|
|
17
13
|
const body = await res.json();
|
|
18
|
-
if (!res.ok || !body.
|
|
14
|
+
if (!res.ok || !body.ok || !body.userId) {
|
|
19
15
|
return {
|
|
20
16
|
ok: false,
|
|
21
|
-
error: pickError(body.
|
|
22
|
-
message: body.
|
|
17
|
+
error: pickError(body.error, res.status),
|
|
18
|
+
message: body.error
|
|
23
19
|
};
|
|
24
20
|
}
|
|
25
21
|
return {
|
|
26
22
|
ok: true,
|
|
27
|
-
user:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
23
|
+
user: {
|
|
24
|
+
id: body.userId,
|
|
25
|
+
email: body.email ?? "",
|
|
26
|
+
name: body.name ?? void 0,
|
|
27
|
+
avatarUrl: body.avatarUrl ?? void 0
|
|
28
|
+
},
|
|
29
|
+
roles: body.roles ?? [],
|
|
30
|
+
scopes: body.scopes ?? [],
|
|
31
|
+
memberships: body.memberships ?? []
|
|
31
32
|
};
|
|
32
33
|
} finally {
|
|
33
34
|
clearTimeout(timer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elvix.is/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"main": "./dist/index.js",
|
|
22
22
|
"types": "./dist/index.d.ts",
|
|
23
23
|
"bin": {
|
|
24
|
+
"elvix": "./dist/cli/index.js",
|
|
24
25
|
"elvix-mcp": "./dist/mcp/bin.js"
|
|
25
26
|
},
|
|
26
27
|
"exports": {
|
|
@@ -45,9 +46,13 @@
|
|
|
45
46
|
"import": "./dist/mcp/index.js"
|
|
46
47
|
}
|
|
47
48
|
},
|
|
48
|
-
"files": [
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
],
|
|
49
54
|
"scripts": {
|
|
50
|
-
"build": "tsup src/index.ts src/react.ts src/server.ts src/types.ts src/mcp/index.ts src/mcp/bin.ts --format esm --dts --clean --external react --external next",
|
|
55
|
+
"build": "tsup src/index.ts src/react.ts src/server.ts src/types.ts src/mcp/index.ts src/mcp/bin.ts src/cli/index.ts src/cli/doctor.ts --format esm --dts --clean --external react --external next",
|
|
51
56
|
"typecheck": "tsc --noEmit",
|
|
52
57
|
"test": "vitest run"
|
|
53
58
|
},
|