limen-auth 0.0.0 → 0.0.1
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 +16 -41
- package/dist/broadcast-channel.mjs +51 -0
- package/dist/build-tree.mjs +73 -0
- package/dist/client.d.mts +7 -0
- package/dist/client.mjs +71 -0
- package/dist/{constants-CsR2pQ_9.mjs → constants.mjs} +1 -3
- package/dist/context.d.mts +32 -0
- package/dist/define-plugin.d.mts +40 -0
- package/dist/{define-plugin-C7WOGU4b.mjs → define-plugin.mjs} +1 -3
- package/dist/envelope.mjs +39 -0
- package/dist/errors.d.mts +19 -0
- package/dist/{errors-4YJYt6f0.mjs → errors.mjs} +1 -3
- package/dist/fetcher.d.mts +17 -0
- package/dist/fetcher.mjs +112 -0
- package/dist/{helpers-CvmKjWi2.d.mts → helpers.d.mts} +1 -2
- package/dist/{helpers-Cs3VtXdv.mjs → helpers.mjs} +1 -3
- package/dist/hooks.d.mts +1 -0
- package/dist/hooks.mjs +34 -0
- package/dist/index.d.mts +14 -30
- package/dist/index.mjs +9 -11
- package/dist/infer.d.mts +42 -0
- package/dist/json-deep-equal.mjs +27 -0
- package/dist/normalize.d.mts +12 -0
- package/dist/normalize.mjs +19 -0
- package/dist/package.mjs +4 -0
- package/dist/path.mjs +41 -0
- package/dist/pipeline.mjs +65 -0
- package/dist/plugin.d.mts +71 -0
- package/dist/plugins/bearer/index.d.mts +13 -1
- package/dist/plugins/bearer/index.mjs +43 -1
- package/dist/plugins/bearer/storage.d.mts +8 -0
- package/dist/plugins/bearer/storage.mjs +41 -0
- package/dist/{index-B8SpHkSd.d.mts → plugins/bearer/types.d.mts} +1 -18
- package/dist/plugins/credential/index.d.mts +54 -1
- package/dist/plugins/credential/index.mjs +2 -4
- package/dist/plugins/credential/types.d.mts +35 -0
- package/dist/plugins/index.d.mts +13 -6
- package/dist/plugins/index.mjs +3 -2
- package/dist/plugins/magic-link/index.d.mts +17 -1
- package/dist/plugins/magic-link/index.mjs +2 -4
- package/dist/plugins/magic-link/types.d.mts +13 -0
- package/dist/plugins/oauth/index.d.mts +47 -1
- package/dist/plugins/oauth/index.mjs +3 -5
- package/dist/plugins/oauth/types.d.mts +35 -0
- package/dist/plugins/session-jwt/index.d.mts +24 -1
- package/dist/plugins/session-jwt/index.mjs +95 -1
- package/dist/plugins/session-jwt/jwt.mjs +34 -0
- package/dist/plugins/session-jwt/types.d.mts +11 -0
- package/dist/plugins/two-factor/index.d.mts +41 -1
- package/dist/plugins/two-factor/index.mjs +2 -4
- package/dist/plugins/two-factor/types.d.mts +26 -0
- package/dist/react/index.d.mts +6 -7
- package/dist/react/index.mjs +2 -20
- package/dist/react/react-store.d.mts +6 -0
- package/dist/react/react-store.mjs +18 -0
- package/dist/route.d.mts +80 -0
- package/dist/{route-DGxvFqWl.mjs → route.mjs} +1 -3
- package/dist/routes.d.mts +53 -0
- package/dist/routes.mjs +58 -0
- package/dist/serialize.d.mts +11 -0
- package/dist/serialize.mjs +23 -0
- package/dist/session-store.d.mts +29 -0
- package/dist/session-store.mjs +79 -0
- package/dist/session-sync.mjs +47 -0
- package/dist/solid/index.d.mts +6 -7
- package/dist/solid/index.mjs +2 -17
- package/dist/solid/solid-store.d.mts +7 -0
- package/dist/solid/solid-store.mjs +15 -0
- package/dist/svelte/index.d.mts +5 -3
- package/dist/svelte/index.mjs +1 -3
- package/dist/type-utils.d.mts +15 -0
- package/dist/types.d.mts +97 -0
- package/dist/version.d.mts +4 -0
- package/dist/version.mjs +5 -0
- package/dist/vue/index.d.mts +6 -19
- package/dist/vue/index.mjs +2 -25
- package/dist/vue/vue-store.d.mts +18 -0
- package/dist/vue/vue-store.mjs +23 -0
- package/package.json +4 -3
- package/dist/bearer-Cqmrmjjf.mjs +0 -84
- package/dist/bearer-Cqmrmjjf.mjs.map +0 -1
- package/dist/client-Er91De-z.mjs +0 -705
- package/dist/client-Er91De-z.mjs.map +0 -1
- package/dist/constants-CsR2pQ_9.mjs.map +0 -1
- package/dist/define-plugin-C7WOGU4b.mjs.map +0 -1
- package/dist/define-plugin-Dv0xXIaH.d.mts +0 -450
- package/dist/define-plugin-Dv0xXIaH.d.mts.map +0 -1
- package/dist/errors-4YJYt6f0.mjs.map +0 -1
- package/dist/helpers-Cs3VtXdv.mjs.map +0 -1
- package/dist/helpers-CvmKjWi2.d.mts.map +0 -1
- package/dist/index-B8SpHkSd.d.mts.map +0 -1
- package/dist/index-C6atwjEq.d.mts +0 -65
- package/dist/index-C6atwjEq.d.mts.map +0 -1
- package/dist/index-C9EuA9UZ.d.mts +0 -32
- package/dist/index-C9EuA9UZ.d.mts.map +0 -1
- package/dist/index-Cgr2wHmM.d.mts +0 -87
- package/dist/index-Cgr2wHmM.d.mts.map +0 -1
- package/dist/index-DV6YcNSu.d.mts +0 -81
- package/dist/index-DV6YcNSu.d.mts.map +0 -1
- package/dist/index-dEksIImj.d.mts +0 -28
- package/dist/index-dEksIImj.d.mts.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/plugins/credential/index.mjs.map +0 -1
- package/dist/plugins/magic-link/index.mjs.map +0 -1
- package/dist/plugins/oauth/index.mjs.map +0 -1
- package/dist/plugins/two-factor/index.mjs.map +0 -1
- package/dist/react/index.d.mts.map +0 -1
- package/dist/react/index.mjs.map +0 -1
- package/dist/route-DGxvFqWl.mjs.map +0 -1
- package/dist/session-jwt-DgLdMQxP.mjs +0 -129
- package/dist/session-jwt-DgLdMQxP.mjs.map +0 -1
- package/dist/solid/index.d.mts.map +0 -1
- package/dist/solid/index.mjs.map +0 -1
- package/dist/svelte/index.d.mts.map +0 -1
- package/dist/svelte/index.mjs.map +0 -1
- package/dist/vue/index.d.mts.map +0 -1
- package/dist/vue/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<a href="https://limenauth.dev">
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="https://raw.githubusercontent.com/thecodearcher/limen/master/banner.svg" alt="Limen" width="640" />
|
|
4
4
|
</a>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/limen-auth"><img src="https://img.shields.io/npm/v/limen-auth?style=flat&colorA=000000&colorB=000000&logo=npm&logoColor=white" alt="npm version" /></a>
|
|
9
|
+
<a href="https://github.com/thecodearcher/limen"><img src="https://img.shields.io/github/stars/thecodearcher/limen?style=flat&colorA=000000&colorB=000000&logo=github" alt="GitHub stars" /></a>
|
|
10
|
+
</p>
|
|
8
11
|
|
|
9
12
|
Official TypeScript client SDK for **[Limen](https://github.com/thecodearcher/limen)** — a modern, composable authentication library for Go. Framework-agnostic core with first-class **React, Vue, Svelte, and Solid** adapters.
|
|
10
13
|
|
|
11
|
-
> 📖 Full guides and API reference: **[limenauth.dev](https://limenauth.dev)**
|
|
12
|
-
|
|
13
14
|
## Install
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
17
|
npm install limen-auth
|
|
17
18
|
```
|
|
18
19
|
|
|
19
|
-
Works with any framework — or none at all. If you're on React, Vue, Svelte, or Solid, just have that framework installed; there's nothing else to add.
|
|
20
|
-
|
|
21
20
|
## Quick start
|
|
22
21
|
|
|
23
22
|
```ts
|
|
@@ -29,44 +28,20 @@ export const auth = createAuthClient({
|
|
|
29
28
|
plugins: [credentialPasswordPlugin()],
|
|
30
29
|
});
|
|
31
30
|
|
|
31
|
+
// `auth.$session` is a reactive store for the current user — it loads on its
|
|
32
|
+
// own, stays in sync across tabs, and updates whenever you sign in or out.
|
|
33
|
+
auth.$session.subscribe(({ data, isPending }) => {
|
|
34
|
+
if (isPending) return;
|
|
35
|
+
console.log(data ? `Signed in as ${data.user.email}` : "Signed out");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Mutations update `$session` automatically — no manual refetch.
|
|
32
39
|
await auth.signIn.credential({ credential: "ada@example.com", password: "secret" });
|
|
33
|
-
const session = await auth.getSession(); // Session | null
|
|
34
40
|
await auth.signout();
|
|
35
41
|
```
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
## Framework adapters
|
|
40
|
-
|
|
41
|
-
Import `createAuthClient` from your framework's entry point and you get a `useSession()` wired to it:
|
|
42
|
-
|
|
43
|
-
```tsx
|
|
44
|
-
import { createAuthClient } from "limen-auth/react";
|
|
45
|
-
import { credentialPasswordPlugin } from "limen-auth/plugins/credential";
|
|
46
|
-
|
|
47
|
-
export const auth = createAuthClient({ baseURL: "...", plugins: [credentialPasswordPlugin()] });
|
|
48
|
-
|
|
49
|
-
function Profile() {
|
|
50
|
-
const { data, isPending } = auth.useSession();
|
|
51
|
-
if (isPending) return <p>Loading…</p>;
|
|
52
|
-
return data ? <p>Hi {data.user.email}</p> : <p>Signed out</p>;
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Also available from `limen-auth/vue`, `limen-auth/svelte`, and `limen-auth/solid`.
|
|
57
|
-
|
|
58
|
-
## Plugins
|
|
59
|
-
|
|
60
|
-
Add the sign-in flows you need as plugins (each lives under `limen-auth/plugins/<name>`):
|
|
61
|
-
|
|
62
|
-
- `credentialPasswordPlugin` — email/username + password
|
|
63
|
-
- `oauthClientPlugin` — social / OAuth providers
|
|
64
|
-
- `magicLinkPlugin` — passwordless email links
|
|
65
|
-
- `twoFactorPlugin` — TOTP, OTP, and backup codes
|
|
66
|
-
- `bearerPlugin` / `sessionJwtPlugin` — token-based sessions
|
|
67
|
-
|
|
68
|
-
See the plugin and full API reference at **[limenauth.dev](https://limenauth.dev)**.
|
|
43
|
+
Using a framework? `limen-auth/react`, `/vue`, `/svelte`, and `/solid` give you a `useSession()` hook over the same store.
|
|
69
44
|
|
|
70
|
-
##
|
|
45
|
+
## Documentation
|
|
71
46
|
|
|
72
|
-
|
|
47
|
+
Full guides, framework adapters (React, Vue, Svelte, Solid), the plugin catalog, and the complete API reference live at **[limenauth.dev](https://limenauth.dev)**.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/broadcast-channel.ts
|
|
2
|
+
const NOOP_PORT = {
|
|
3
|
+
post() {},
|
|
4
|
+
subscribe: () => () => {},
|
|
5
|
+
close() {}
|
|
6
|
+
};
|
|
7
|
+
function createBroadcastChannel(name) {
|
|
8
|
+
if (typeof window === "undefined") return NOOP_PORT;
|
|
9
|
+
const storageKey = `${name}-sync`;
|
|
10
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
11
|
+
const emit = (message) => {
|
|
12
|
+
for (const listener of listeners) listener(message);
|
|
13
|
+
};
|
|
14
|
+
const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(name) : null;
|
|
15
|
+
if (channel) channel.onmessage = (event) => {
|
|
16
|
+
if (event.data != null) emit(event.data);
|
|
17
|
+
};
|
|
18
|
+
const onStorage = (event) => {
|
|
19
|
+
if (event.key !== storageKey || !event.newValue) return;
|
|
20
|
+
emit(JSON.parse(event.newValue));
|
|
21
|
+
};
|
|
22
|
+
if (!channel) window.addEventListener("storage", onStorage);
|
|
23
|
+
return {
|
|
24
|
+
post(message) {
|
|
25
|
+
if (channel) {
|
|
26
|
+
channel.postMessage(message);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (typeof globalThis.localStorage !== "undefined") {
|
|
30
|
+
globalThis.localStorage.setItem(storageKey, JSON.stringify(message));
|
|
31
|
+
globalThis.localStorage.removeItem(storageKey);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
subscribe(listener) {
|
|
35
|
+
listeners.add(listener);
|
|
36
|
+
return () => {
|
|
37
|
+
listeners.delete(listener);
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
close() {
|
|
41
|
+
listeners.clear();
|
|
42
|
+
if (channel) {
|
|
43
|
+
channel.close();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
window.removeEventListener("storage", onStorage);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { createBroadcastChannel };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ensureLeadingSlash, kebabToCamel, normalizeBasePath } from "./helpers.mjs";
|
|
2
|
+
import { chainFromDotted, pathToChain } from "./path.mjs";
|
|
3
|
+
import { runRoute } from "./pipeline.mjs";
|
|
4
|
+
//#region src/build-tree.ts
|
|
5
|
+
/** Scope a context's `fetch` to one plugin's base path (after client `overrides`). */
|
|
6
|
+
function scopeContext(ctx, fetcher, plugin, overrides) {
|
|
7
|
+
const defaultBase = normalizeBasePath(plugin.basePath ?? "");
|
|
8
|
+
const overrideBase = overrides?.[kebabToCamel(plugin.id)]?.basePath;
|
|
9
|
+
const resolvedBase = normalizeBasePath(overrideBase ?? plugin.basePath ?? "");
|
|
10
|
+
return {
|
|
11
|
+
...ctx,
|
|
12
|
+
fetch: (path, init) => {
|
|
13
|
+
const absolute = init?.absolute === true;
|
|
14
|
+
const requestPath = (absolute ? "" : resolvedBase) + ensureLeadingSlash(path);
|
|
15
|
+
const routePath = (absolute ? "" : defaultBase) + ensureLeadingSlash(path);
|
|
16
|
+
return fetcher.fetch(requestPath, init, routePath);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function chainFor(plugin, def) {
|
|
21
|
+
if (typeof def.as === "string") return chainFromDotted(def.as);
|
|
22
|
+
return [...pathToChain(plugin.basePath ?? ""), ...pathToChain(def.path)];
|
|
23
|
+
}
|
|
24
|
+
function mountAtChain(target, pathSegments, callable) {
|
|
25
|
+
let current = target;
|
|
26
|
+
for (let i = 0; i < pathSegments.length - 1; i += 1) {
|
|
27
|
+
const segment = pathSegments[i];
|
|
28
|
+
const child = current[segment];
|
|
29
|
+
if (child === void 0) {
|
|
30
|
+
const namespace = {};
|
|
31
|
+
current[segment] = namespace;
|
|
32
|
+
current = namespace;
|
|
33
|
+
} else current = child;
|
|
34
|
+
}
|
|
35
|
+
const finalSegment = pathSegments[pathSegments.length - 1];
|
|
36
|
+
current[finalSegment] = callable;
|
|
37
|
+
}
|
|
38
|
+
function isNamespace(value) {
|
|
39
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value !== "function";
|
|
40
|
+
}
|
|
41
|
+
function mergeInto(target, source) {
|
|
42
|
+
for (const [key, value] of Object.entries(source)) {
|
|
43
|
+
const existing = target[key];
|
|
44
|
+
if (isNamespace(existing) && isNamespace(value)) {
|
|
45
|
+
mergeInto(existing, value);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
target[key] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Build the public API object from plugin routes and actions.
|
|
53
|
+
*/
|
|
54
|
+
function buildClientTree({ plugins, ctx, fetcher, overrides }) {
|
|
55
|
+
const api = {};
|
|
56
|
+
for (const plugin of plugins) {
|
|
57
|
+
const scopedCtx = scopeContext(ctx, fetcher, plugin, overrides);
|
|
58
|
+
const contribution = {};
|
|
59
|
+
for (const def of plugin.routes) {
|
|
60
|
+
if (def.expose === false) continue;
|
|
61
|
+
const call = (input, opts) => runRoute(scopedCtx, def, input, opts);
|
|
62
|
+
mountAtChain(contribution, chainFor(plugin, def), call);
|
|
63
|
+
}
|
|
64
|
+
if (plugin.actions !== void 0) {
|
|
65
|
+
const run = (route, input) => runRoute(scopedCtx, route, input);
|
|
66
|
+
mergeInto(contribution, plugin.actions(scopedCtx, run));
|
|
67
|
+
}
|
|
68
|
+
mergeInto(api, contribution);
|
|
69
|
+
}
|
|
70
|
+
return api;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { buildClientTree };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AuthClient, CreateAuthClientOptions } from "./types.mjs";
|
|
2
|
+
import { AnyClientPlugin } from "./define-plugin.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/client.d.ts
|
|
5
|
+
declare function createAuthClient<const Plugins extends readonly AnyClientPlugin[] = readonly [], TFields = unknown>(opts: CreateAuthClientOptions<Plugins, TFields>): AuthClient<Plugins, TFields>;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { createAuthClient };
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { normalizeBasePath, stripTrailingSlash } from "./helpers.mjs";
|
|
2
|
+
import { buildClientTree } from "./build-tree.mjs";
|
|
3
|
+
import { DEFAULT_ENVELOPE_CONFIG } from "./constants.mjs";
|
|
4
|
+
import { Fetcher } from "./fetcher.mjs";
|
|
5
|
+
import { HookRunner } from "./hooks.mjs";
|
|
6
|
+
import { defaultSessionParse } from "./normalize.mjs";
|
|
7
|
+
import { coreClientPlugin, createSessionHydrator } from "./routes.mjs";
|
|
8
|
+
import { createSessionStore } from "./session-store.mjs";
|
|
9
|
+
//#region src/client.ts
|
|
10
|
+
function createAuthClient(opts) {
|
|
11
|
+
const baseURL = stripTrailingSlash(opts.baseURL);
|
|
12
|
+
const basePath = normalizeBasePath(opts.basePath ?? "/auth");
|
|
13
|
+
const userPlugins = opts.plugins ?? [];
|
|
14
|
+
const plugins = [coreClientPlugin(), ...userPlugins];
|
|
15
|
+
const hooks = new HookRunner(plugins);
|
|
16
|
+
const fetcher = buildFetcher(baseURL, basePath, {
|
|
17
|
+
...DEFAULT_ENVELOPE_CONFIG,
|
|
18
|
+
...opts.envelope
|
|
19
|
+
}, hooks, opts.fetchOptions ?? {});
|
|
20
|
+
const parseSession = opts.parseSession ?? defaultSessionParse;
|
|
21
|
+
const redirect = resolveRedirect(opts.redirectFn);
|
|
22
|
+
const baseFetch = (path, init) => fetcher.fetch(path, init);
|
|
23
|
+
const store = createSessionStore({
|
|
24
|
+
hydrator: createSessionHydrator({
|
|
25
|
+
fetch: baseFetch,
|
|
26
|
+
parseSession
|
|
27
|
+
}),
|
|
28
|
+
crossTabSync: opts.crossTabSync !== false,
|
|
29
|
+
refetchOnWindowFocus: opts.refetchOnWindowFocus !== false,
|
|
30
|
+
...opts.initialSession !== void 0 ? { initialSession: opts.initialSession } : {}
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
baseURL,
|
|
34
|
+
basePath,
|
|
35
|
+
...buildClientTree({
|
|
36
|
+
plugins,
|
|
37
|
+
ctx: {
|
|
38
|
+
fetch: baseFetch,
|
|
39
|
+
redirect,
|
|
40
|
+
parseSession,
|
|
41
|
+
setSession: (session) => store.setData(session),
|
|
42
|
+
refetchSession: () => store.refetch(),
|
|
43
|
+
store
|
|
44
|
+
},
|
|
45
|
+
fetcher,
|
|
46
|
+
overrides: opts.overrides
|
|
47
|
+
}),
|
|
48
|
+
$session: store.$session
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function buildFetcher(baseURL, basePath, envelope, hooks, fetchOptions) {
|
|
52
|
+
return new Fetcher({
|
|
53
|
+
baseURL,
|
|
54
|
+
basePath,
|
|
55
|
+
envelope,
|
|
56
|
+
hooks,
|
|
57
|
+
fetchOptions
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function resolveRedirect(redirect) {
|
|
61
|
+
return (url) => {
|
|
62
|
+
if (redirect !== void 0) return redirect(url);
|
|
63
|
+
if (typeof window !== "undefined" && typeof window.location !== "undefined") {
|
|
64
|
+
window.location.href = url;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { createAuthClient };
|
|
@@ -10,6 +10,4 @@ const SET_REFRESH_TOKEN_HEADER = "Set-Refresh-Token";
|
|
|
10
10
|
/** Default `localStorage` key for the persisted tokens. */
|
|
11
11
|
const DEFAULT_TOKEN_STORAGE_KEY = "limen.tokens";
|
|
12
12
|
//#endregion
|
|
13
|
-
export {
|
|
14
|
-
|
|
15
|
-
//# sourceMappingURL=constants-CsR2pQ_9.mjs.map
|
|
13
|
+
export { DEFAULT_ENVELOPE_CONFIG, DEFAULT_ENVELOPE_FIELDS, DEFAULT_TOKEN_STORAGE_KEY, SET_AUTH_TOKEN_HEADER, SET_REFRESH_TOKEN_HEADER };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SessionStore } from "./session-store.mjs";
|
|
2
|
+
import { ParseSession, RedirectFn, Session } from "./types.mjs";
|
|
3
|
+
import { FetchInit } from "./plugin.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/context.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Context passed to route handlers and plugin actions.
|
|
8
|
+
*/
|
|
9
|
+
type RouteContext<TFields = unknown> = {
|
|
10
|
+
/**
|
|
11
|
+
* Fetch a path relative to the plugin base path. Pass
|
|
12
|
+
* `init.absolute = true` to resolve from the client base path instead.
|
|
13
|
+
*/
|
|
14
|
+
fetch: <T>(path: string, init?: FetchInit) => Promise<T>;
|
|
15
|
+
/**
|
|
16
|
+
* Navigate to an absolute URL. Returns whether navigation happened.
|
|
17
|
+
*/
|
|
18
|
+
readonly redirect: RedirectFn;
|
|
19
|
+
/**
|
|
20
|
+
* Parse a session-bearing response.
|
|
21
|
+
*/
|
|
22
|
+
readonly parseSession: ParseSession<TFields>;
|
|
23
|
+
/**
|
|
24
|
+
* Write a session into the reactive store, or `null` to clear it.
|
|
25
|
+
*/
|
|
26
|
+
setSession: (session: Session<TFields> | null) => void; /** Revalidate the current session. */
|
|
27
|
+
refetchSession: () => Promise<void>; /** Reactive session store. */
|
|
28
|
+
readonly store: SessionStore<TFields>;
|
|
29
|
+
};
|
|
30
|
+
type AnyRouteContext = RouteContext<any>;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { AnyRouteContext, RouteContext };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { IsAny, UnionToIntersection } from "./type-utils.mjs";
|
|
2
|
+
import { AnyRouteDescriptor, RouteDescriptor } from "./route.mjs";
|
|
3
|
+
import { PluginHooks } from "./plugin.mjs";
|
|
4
|
+
import { RouteContext } from "./context.mjs";
|
|
5
|
+
import { InferRoutes, PathSegments } from "./infer.mjs";
|
|
6
|
+
|
|
7
|
+
//#region src/define-plugin.d.ts
|
|
8
|
+
/** Invoke one of the plugin's routes from `actions`. */
|
|
9
|
+
type RunRoute = <I, O>(route: RouteDescriptor<I, O>, input: I) => Promise<O>;
|
|
10
|
+
/**
|
|
11
|
+
* Client plugin contract
|
|
12
|
+
*/
|
|
13
|
+
type ClientPlugin<Id extends string, BasePath extends string, Routes extends readonly AnyRouteDescriptor[], Actions> = {
|
|
14
|
+
readonly id: Id; /** Default mount path relative to the client `basePath`, e.g. `"/magic-link"`; omit for the root (`""`). */
|
|
15
|
+
readonly basePath?: BasePath;
|
|
16
|
+
readonly routes: Routes;
|
|
17
|
+
readonly hooks?: PluginHooks;
|
|
18
|
+
readonly actions?: (ctx: RouteContext, run: RunRoute) => Actions;
|
|
19
|
+
};
|
|
20
|
+
type AnyClientPlugin = ClientPlugin<string, string, readonly AnyRouteDescriptor[], any>;
|
|
21
|
+
/**
|
|
22
|
+
* Register a plugin's routes while preserving each route's input/output types
|
|
23
|
+
* for the generated client API.
|
|
24
|
+
*/
|
|
25
|
+
declare function defineRoutes<Routes extends readonly AnyRouteDescriptor[]>(...routes: Routes): Routes;
|
|
26
|
+
/**
|
|
27
|
+
* Define a client plugin for `createAuthClient`.
|
|
28
|
+
*/
|
|
29
|
+
declare function defineClientPlugin<const Id extends string, const Routes extends readonly AnyRouteDescriptor[], Actions = Record<never, never>, const BasePath extends string = "">(def: ClientPlugin<Id, BasePath, Routes, Actions>): ClientPlugin<Id, BasePath, Routes, Actions>;
|
|
30
|
+
/**
|
|
31
|
+
* The client-only methods a plugin contributes. Widened `any` contributes
|
|
32
|
+
* nothing so the client does not become permissive.
|
|
33
|
+
*/
|
|
34
|
+
type ActionsOf<P> = P extends {
|
|
35
|
+
actions?: (ctx: never, run: never) => infer A;
|
|
36
|
+
} ? IsAny<A> extends true ? unknown : A extends object ? A : unknown : unknown;
|
|
37
|
+
type InferPluginContribution<P> = P extends ClientPlugin<infer _Id, infer BasePath, infer Routes, infer _Actions> ? InferRoutes<Routes, PathSegments<BasePath>> & ActionsOf<P> : unknown;
|
|
38
|
+
type CombinedClientContributions<Plugins extends readonly unknown[]> = UnionToIntersection<{ [K in keyof Plugins]: InferPluginContribution<Plugins[K]> }[number]>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { AnyClientPlugin, ClientPlugin, CombinedClientContributions, InferPluginContribution, RunRoute, defineClientPlugin, defineRoutes };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DEFAULT_ENVELOPE_FIELDS } from "./constants.mjs";
|
|
2
|
+
//#region src/envelope.ts
|
|
3
|
+
/**
|
|
4
|
+
* Unwrap a parsed JSON success body according to envelope config.
|
|
5
|
+
*
|
|
6
|
+
* - `mode: "off"`: return as-is.
|
|
7
|
+
* - `mode: "wrap-success" | "always"`: extract `body[fields.data]` if present;
|
|
8
|
+
* if the key is missing (server didn't wrap this particular response), fall
|
|
9
|
+
* back to the raw body so we don't lose data.
|
|
10
|
+
*
|
|
11
|
+
* Returning `unknown` is intentional: the caller knows the expected shape and
|
|
12
|
+
* narrows / validates it. Trying to be clever here would invite false typing.
|
|
13
|
+
*/
|
|
14
|
+
function unwrapPayload(body, envelope) {
|
|
15
|
+
if (envelope.mode === "off") return body;
|
|
16
|
+
if (body === null || typeof body !== "object") return body;
|
|
17
|
+
const fields = envelope.fields ?? DEFAULT_ENVELOPE_FIELDS;
|
|
18
|
+
const record = body;
|
|
19
|
+
if (fields.data in record) return record[fields.data];
|
|
20
|
+
return body;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Pull the human-readable error message out of a non-2xx body.
|
|
24
|
+
*
|
|
25
|
+
* - `mode: "off" | "wrap-success"`: error bodies look like `{ message }`
|
|
26
|
+
* - `mode: "always"`: error bodies use the configured `fields.message` key.
|
|
27
|
+
*
|
|
28
|
+
* Returns `undefined` when no message can be located; the caller substitutes
|
|
29
|
+
* a status-based fallback (e.g. HTTP status text).
|
|
30
|
+
*/
|
|
31
|
+
function unwrapErrorMessage(body, envelope) {
|
|
32
|
+
if (body === null || typeof body !== "object") return;
|
|
33
|
+
const record = body;
|
|
34
|
+
const fields = envelope.fields ?? DEFAULT_ENVELOPE_FIELDS;
|
|
35
|
+
const value = record[envelope.mode === "always" ? fields.message : "message"];
|
|
36
|
+
return typeof value === "string" ? value : void 0;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { unwrapErrorMessage, unwrapPayload };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
type LimenErrorCode = "unauthorized" | "forbidden" | "not_found" | "rate_limited" | "validation_error" | "conflict" | "server_error" | "unknown";
|
|
3
|
+
/** Map HTTP status → typed code. Anything unmapped becomes `"unknown"`. */
|
|
4
|
+
declare function deriveErrorCode(status: number): LimenErrorCode;
|
|
5
|
+
/**
|
|
6
|
+
* The single error type every SDK call throws on non-2xx. Carries the raw
|
|
7
|
+
* server message, the HTTP status, and a derived typed code. Subclass-free so
|
|
8
|
+
* `instanceof LimenError` is the only check consumers need.
|
|
9
|
+
*/
|
|
10
|
+
declare class LimenError extends Error {
|
|
11
|
+
readonly name = "LimenError";
|
|
12
|
+
readonly status: number;
|
|
13
|
+
readonly code: LimenErrorCode;
|
|
14
|
+
constructor(message: string, status: number, code?: LimenErrorCode);
|
|
15
|
+
get isUnauthorized(): boolean;
|
|
16
|
+
get isRateLimited(): boolean;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { LimenError, LimenErrorCode, deriveErrorCode };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ResponseContext } from "./plugin.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/fetcher.d.ts
|
|
4
|
+
type ClientFetchCallbackContext = ResponseContext & {};
|
|
5
|
+
type FetcherFetchOptions = {
|
|
6
|
+
/** Whether to send credentials (cookies). Defaults to `"include"`. */credentials?: RequestCredentials; /** Custom fetch impl. Defaults to `globalThis.fetch`. */
|
|
7
|
+
impl?: typeof fetch; /** Default headers merged into every request. Per-request headers override these. */
|
|
8
|
+
headers?: HeadersInit; /** Callback function to be called when the request is successful. */
|
|
9
|
+
onSuccess?: (context: ClientFetchCallbackContext & {
|
|
10
|
+
response: Response;
|
|
11
|
+
}) => void; /** Callback function to be called when the request fails. */
|
|
12
|
+
onError?: (context: ClientFetchCallbackContext & {
|
|
13
|
+
error: Error;
|
|
14
|
+
}) => void;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { FetcherFetchOptions };
|
package/dist/fetcher.mjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ensureLeadingSlash, joinURL, stripTrailingSlash } from "./helpers.mjs";
|
|
2
|
+
import { unwrapErrorMessage, unwrapPayload } from "./envelope.mjs";
|
|
3
|
+
import { LimenError, deriveErrorCode } from "./errors.mjs";
|
|
4
|
+
//#region src/fetcher.ts
|
|
5
|
+
var Fetcher = class {
|
|
6
|
+
opts;
|
|
7
|
+
fetchImpl;
|
|
8
|
+
credentials;
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
this.fetchImpl = opts.fetchOptions.impl ?? globalThis.fetch.bind(globalThis);
|
|
12
|
+
this.credentials = opts.fetchOptions.credentials ?? "include";
|
|
13
|
+
}
|
|
14
|
+
async fetch(path, init, routePath = path) {
|
|
15
|
+
const method = init?.method ?? (init?.body !== void 0 ? "POST" : "GET");
|
|
16
|
+
return this.run({
|
|
17
|
+
method,
|
|
18
|
+
path,
|
|
19
|
+
routePath,
|
|
20
|
+
body: init?.body,
|
|
21
|
+
headers: init?.headers,
|
|
22
|
+
query: init?.query
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds the request context, runs hooks, does
|
|
27
|
+
* the fetch, parses the response, runs hooks again, throws on non-2xx.
|
|
28
|
+
*/
|
|
29
|
+
async run(args) {
|
|
30
|
+
const fullPath = this.normalizeRelativePath(this.opts.basePath, args.path, args.query);
|
|
31
|
+
const url = joinURL(this.opts.baseURL, fullPath);
|
|
32
|
+
const headers = new Headers({
|
|
33
|
+
...args.headers,
|
|
34
|
+
...this.opts.fetchOptions.headers ?? {}
|
|
35
|
+
});
|
|
36
|
+
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
37
|
+
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
38
|
+
let reqCtx = {
|
|
39
|
+
method: args.method,
|
|
40
|
+
fullPath,
|
|
41
|
+
path: args.path,
|
|
42
|
+
routePath: args.routePath,
|
|
43
|
+
url,
|
|
44
|
+
headers,
|
|
45
|
+
body: args.body
|
|
46
|
+
};
|
|
47
|
+
reqCtx = await this.opts.hooks.runBeforeRequest(reqCtx);
|
|
48
|
+
const payload = reqCtx.body !== void 0 && reqCtx.body !== null ? JSON.stringify(reqCtx.body) : void 0;
|
|
49
|
+
const requestInit = {
|
|
50
|
+
method: reqCtx.method,
|
|
51
|
+
headers: reqCtx.headers,
|
|
52
|
+
credentials: this.credentials
|
|
53
|
+
};
|
|
54
|
+
if (payload !== void 0) requestInit.body = payload;
|
|
55
|
+
let response;
|
|
56
|
+
try {
|
|
57
|
+
response = await this.fetchImpl(reqCtx.url, requestInit);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
this.opts.fetchOptions.onError?.({
|
|
60
|
+
...reqCtx,
|
|
61
|
+
status: 0,
|
|
62
|
+
ok: false,
|
|
63
|
+
error: err
|
|
64
|
+
});
|
|
65
|
+
throw new LimenError(err instanceof Error ? err.message : "Network request failed", 0, "unknown");
|
|
66
|
+
}
|
|
67
|
+
const parsedBody = await this.parseResponseBody(response);
|
|
68
|
+
const unwrapped = response.ok && parsedBody !== void 0 ? unwrapPayload(parsedBody, this.opts.envelope) : parsedBody;
|
|
69
|
+
let resCtx = {
|
|
70
|
+
method: args.method,
|
|
71
|
+
fullPath,
|
|
72
|
+
path: args.path,
|
|
73
|
+
routePath: args.routePath,
|
|
74
|
+
status: response.status,
|
|
75
|
+
ok: response.ok,
|
|
76
|
+
headers: response.headers,
|
|
77
|
+
body: unwrapped
|
|
78
|
+
};
|
|
79
|
+
resCtx = await this.opts.hooks.runAfterResponse(resCtx);
|
|
80
|
+
if (resCtx.ok) {
|
|
81
|
+
this.opts.fetchOptions.onSuccess?.({
|
|
82
|
+
...resCtx,
|
|
83
|
+
response
|
|
84
|
+
});
|
|
85
|
+
return resCtx.body;
|
|
86
|
+
}
|
|
87
|
+
const error = new LimenError(unwrapErrorMessage(resCtx.body, this.opts.envelope) ?? response.statusText ?? `Request failed with status ${response.status}`, response.status, deriveErrorCode(response.status));
|
|
88
|
+
this.opts.fetchOptions.onError?.({
|
|
89
|
+
...reqCtx,
|
|
90
|
+
status: response.status,
|
|
91
|
+
ok: false,
|
|
92
|
+
error
|
|
93
|
+
});
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
async parseResponseBody(response) {
|
|
97
|
+
if (response.status === 204) return;
|
|
98
|
+
const text = await response.text();
|
|
99
|
+
if (text.length === 0) return;
|
|
100
|
+
return JSON.parse(text);
|
|
101
|
+
}
|
|
102
|
+
normalizeRelativePath(basePath, relativePath, query) {
|
|
103
|
+
let path = (basePath === "" || basePath === "/" ? "" : ensureLeadingSlash(stripTrailingSlash(basePath))) + ensureLeadingSlash(relativePath);
|
|
104
|
+
if (query !== void 0) {
|
|
105
|
+
const qs = new URLSearchParams(query).toString();
|
|
106
|
+
if (qs.length > 0) path = `${path}?${qs}`;
|
|
107
|
+
}
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
//#endregion
|
|
112
|
+
export { Fetcher };
|
|
@@ -6,5 +6,4 @@
|
|
|
6
6
|
declare function camelizeKeys<T = Record<string, unknown>>(raw: unknown): T;
|
|
7
7
|
declare function camelizeEach<T = Record<string, unknown>>(raw: unknown): T[];
|
|
8
8
|
//#endregion
|
|
9
|
-
export {
|
|
10
|
-
//# sourceMappingURL=helpers-CvmKjWi2.d.mts.map
|
|
9
|
+
export { camelizeEach, camelizeKeys };
|
|
@@ -49,6 +49,4 @@ function camelToSnake(key) {
|
|
|
49
49
|
return key.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`);
|
|
50
50
|
}
|
|
51
51
|
//#endregion
|
|
52
|
-
export {
|
|
53
|
-
|
|
54
|
-
//# sourceMappingURL=helpers-Cs3VtXdv.mjs.map
|
|
52
|
+
export { camelToSnake, camelizeEach, camelizeKeys, ensureLeadingSlash, joinURL, kebabToCamel, normalizeBasePath, stripTrailingSlash, toCamelCaseKey };
|
package/dist/hooks.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/hooks.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/hooks.ts
|
|
2
|
+
function matchesRoute(matcher, routePath) {
|
|
3
|
+
if (matcher === void 0) return true;
|
|
4
|
+
if (typeof matcher === "string") return routePath === matcher;
|
|
5
|
+
if (typeof matcher === "function") return matcher({ path: routePath });
|
|
6
|
+
return routePath !== void 0 && matcher.includes(routePath);
|
|
7
|
+
}
|
|
8
|
+
var HookRunner = class {
|
|
9
|
+
before;
|
|
10
|
+
after;
|
|
11
|
+
constructor(plugins) {
|
|
12
|
+
this.before = plugins.flatMap((p) => p.hooks?.beforeRequest ?? []);
|
|
13
|
+
this.after = plugins.flatMap((p) => p.hooks?.afterResponse ?? []);
|
|
14
|
+
}
|
|
15
|
+
async runBeforeRequest(initial) {
|
|
16
|
+
let ctx = initial;
|
|
17
|
+
for (const hook of this.before) {
|
|
18
|
+
if (!matchesRoute(hook.match, ctx.routePath)) continue;
|
|
19
|
+
ctx = await hook.run(ctx);
|
|
20
|
+
}
|
|
21
|
+
return ctx;
|
|
22
|
+
}
|
|
23
|
+
async runAfterResponse(initial) {
|
|
24
|
+
let ctx = initial;
|
|
25
|
+
for (const hook of this.after) {
|
|
26
|
+
if (!matchesRoute(hook.match, ctx.routePath)) continue;
|
|
27
|
+
if (!hook.allowOnFailure && ctx.status >= 400) continue;
|
|
28
|
+
ctx = await hook.run(ctx);
|
|
29
|
+
}
|
|
30
|
+
return ctx;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { HookRunner };
|