limen-auth 0.0.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 +21 -0
- package/README.md +72 -0
- package/dist/bearer-Cqmrmjjf.mjs +84 -0
- package/dist/bearer-Cqmrmjjf.mjs.map +1 -0
- package/dist/client-Er91De-z.mjs +705 -0
- package/dist/client-Er91De-z.mjs.map +1 -0
- package/dist/constants-CsR2pQ_9.mjs +15 -0
- package/dist/constants-CsR2pQ_9.mjs.map +1 -0
- package/dist/define-plugin-C7WOGU4b.mjs +24 -0
- package/dist/define-plugin-C7WOGU4b.mjs.map +1 -0
- package/dist/define-plugin-Dv0xXIaH.d.mts +450 -0
- package/dist/define-plugin-Dv0xXIaH.d.mts.map +1 -0
- package/dist/errors-4YJYt6f0.mjs +37 -0
- package/dist/errors-4YJYt6f0.mjs.map +1 -0
- package/dist/helpers-Cs3VtXdv.mjs +54 -0
- package/dist/helpers-Cs3VtXdv.mjs.map +1 -0
- package/dist/helpers-CvmKjWi2.d.mts +10 -0
- package/dist/helpers-CvmKjWi2.d.mts.map +1 -0
- package/dist/index-B8SpHkSd.d.mts +48 -0
- package/dist/index-B8SpHkSd.d.mts.map +1 -0
- package/dist/index-C6atwjEq.d.mts +65 -0
- package/dist/index-C6atwjEq.d.mts.map +1 -0
- package/dist/index-C9EuA9UZ.d.mts +32 -0
- package/dist/index-C9EuA9UZ.d.mts.map +1 -0
- package/dist/index-Cgr2wHmM.d.mts +87 -0
- package/dist/index-Cgr2wHmM.d.mts.map +1 -0
- package/dist/index-DV6YcNSu.d.mts +81 -0
- package/dist/index-DV6YcNSu.d.mts.map +1 -0
- package/dist/index-dEksIImj.d.mts +28 -0
- package/dist/index-dEksIImj.d.mts.map +1 -0
- package/dist/index.d.mts +30 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +1 -0
- package/dist/plugins/bearer/index.d.mts +2 -0
- package/dist/plugins/bearer/index.mjs +2 -0
- package/dist/plugins/credential/index.d.mts +2 -0
- package/dist/plugins/credential/index.mjs +50 -0
- package/dist/plugins/credential/index.mjs.map +1 -0
- package/dist/plugins/index.d.mts +7 -0
- package/dist/plugins/index.mjs +7 -0
- package/dist/plugins/magic-link/index.d.mts +2 -0
- package/dist/plugins/magic-link/index.mjs +21 -0
- package/dist/plugins/magic-link/index.mjs.map +1 -0
- package/dist/plugins/oauth/index.d.mts +2 -0
- package/dist/plugins/oauth/index.mjs +56 -0
- package/dist/plugins/oauth/index.mjs.map +1 -0
- package/dist/plugins/session-jwt/index.d.mts +2 -0
- package/dist/plugins/session-jwt/index.mjs +2 -0
- package/dist/plugins/two-factor/index.d.mts +2 -0
- package/dist/plugins/two-factor/index.mjs +52 -0
- package/dist/plugins/two-factor/index.mjs.map +1 -0
- package/dist/react/index.d.mts +24 -0
- package/dist/react/index.d.mts.map +1 -0
- package/dist/react/index.mjs +31 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/route-DGxvFqWl.mjs +19 -0
- package/dist/route-DGxvFqWl.mjs.map +1 -0
- package/dist/session-jwt-DgLdMQxP.mjs +129 -0
- package/dist/session-jwt-DgLdMQxP.mjs.map +1 -0
- package/dist/solid/index.d.mts +25 -0
- package/dist/solid/index.d.mts.map +1 -0
- package/dist/solid/index.mjs +28 -0
- package/dist/solid/index.mjs.map +1 -0
- package/dist/svelte/index.d.mts +25 -0
- package/dist/svelte/index.d.mts.map +1 -0
- package/dist/svelte/index.mjs +18 -0
- package/dist/svelte/index.mjs.map +1 -0
- package/dist/vue/index.d.mts +36 -0
- package/dist/vue/index.d.mts.map +1 -0
- package/dist/vue/index.mjs +36 -0
- package/dist/vue/index.mjs.map +1 -0
- package/package.json +123 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { a as joinURL, c as stripTrailingSlash, i as ensureLeadingSlash, l as toCamelCaseKey, n as camelizeEach, o as kebabToCamel, s as normalizeBasePath, t as camelToSnake } from "./helpers-Cs3VtXdv.mjs";
|
|
2
|
+
import { n as DEFAULT_ENVELOPE_FIELDS, t as DEFAULT_ENVELOPE_CONFIG } from "./constants-CsR2pQ_9.mjs";
|
|
3
|
+
import { n as deriveErrorCode, t as LimenError } from "./errors-4YJYt6f0.mjs";
|
|
4
|
+
import { n as defineRoutes, t as defineClientPlugin } from "./define-plugin-C7WOGU4b.mjs";
|
|
5
|
+
import { t as route } from "./route-DGxvFqWl.mjs";
|
|
6
|
+
import { atom, onMount, onNotify } from "nanostores";
|
|
7
|
+
//#region src/path.ts
|
|
8
|
+
/**
|
|
9
|
+
* Derive the public client chain from a route path. Mirrors the type-level
|
|
10
|
+
* `PathSegments` in `infer.ts` exactly, so runtime materialization and compile
|
|
11
|
+
* time inference can never drift.
|
|
12
|
+
*
|
|
13
|
+
* "/otp/send" -> ["otp", "send"]
|
|
14
|
+
* "/revoke-sessions" -> ["revokeSessions"]
|
|
15
|
+
* "/:provider/authorize" -> ["authorize"] (param segments are dropped)
|
|
16
|
+
*/
|
|
17
|
+
function pathToChain(path) {
|
|
18
|
+
return path.split("/").filter((seg) => seg.length > 0 && !seg.startsWith(":")).map(kebabToCamel);
|
|
19
|
+
}
|
|
20
|
+
function chainFromDotted(chain) {
|
|
21
|
+
return chain.split(".").filter((seg) => seg.length > 0);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Substitute declared path params from `input` into `path` and strip them from
|
|
25
|
+
* the payload.
|
|
26
|
+
*/
|
|
27
|
+
function resolvePath(path, params, input) {
|
|
28
|
+
if (!params || params.length === 0) return {
|
|
29
|
+
path,
|
|
30
|
+
rest: input
|
|
31
|
+
};
|
|
32
|
+
const rest = { ...input ?? {} };
|
|
33
|
+
let resolved = path;
|
|
34
|
+
for (const param of params) {
|
|
35
|
+
const value = rest[param];
|
|
36
|
+
if (value === void 0) throw new Error(`Missing required path param "${param}" for route "${path}"`);
|
|
37
|
+
resolved = resolved.replace(`:${param}`, encodeURIComponent(value));
|
|
38
|
+
delete rest[param];
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
path: resolved,
|
|
42
|
+
rest
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/serialize.ts
|
|
47
|
+
/**
|
|
48
|
+
* Default request serializer: shallow camelCase → snake_case, drops `undefined`,
|
|
49
|
+
* leaves non-objects unchanged.
|
|
50
|
+
*
|
|
51
|
+
* `additionalFields` entries are merged into the top-level body verbatim.
|
|
52
|
+
* Known route fields win on key collisions.
|
|
53
|
+
*/
|
|
54
|
+
function defaultSerialize(input) {
|
|
55
|
+
if (input === null || input === void 0) return input;
|
|
56
|
+
if (typeof input !== "object" || Array.isArray(input)) return input;
|
|
57
|
+
const { additionalFields, ...rest } = input;
|
|
58
|
+
const out = {};
|
|
59
|
+
if (additionalFields && typeof additionalFields === "object" && !Array.isArray(additionalFields)) Object.assign(out, additionalFields);
|
|
60
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
61
|
+
if (value === void 0) continue;
|
|
62
|
+
out[camelToSnake(key)] = value;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/pipeline.ts
|
|
68
|
+
/**
|
|
69
|
+
* Run the default HTTP steps for a route — merge defaults, resolve path params,
|
|
70
|
+
* serialize, dispatch, parse — without applying session effects.
|
|
71
|
+
*/
|
|
72
|
+
async function runHttp(ctx, def, input) {
|
|
73
|
+
let merged = input;
|
|
74
|
+
if (def.defaults !== void 0) merged = {
|
|
75
|
+
...def.defaults,
|
|
76
|
+
...input ?? {}
|
|
77
|
+
};
|
|
78
|
+
const { path, rest } = resolvePath(def.path, def.params, merged);
|
|
79
|
+
const payload = def.serialize !== void 0 ? def.serialize(rest) : defaultSerialize(rest);
|
|
80
|
+
const init = {
|
|
81
|
+
method: def.method,
|
|
82
|
+
absolute: def.absolute ?? false
|
|
83
|
+
};
|
|
84
|
+
if (def.method === "GET" && payload !== void 0) init.query = payload;
|
|
85
|
+
else init.body = payload;
|
|
86
|
+
const raw = await ctx.fetch(path, init);
|
|
87
|
+
if (def.parseSession === true) return ctx.parseSession(raw);
|
|
88
|
+
if (def.parse !== void 0) return def.parse(raw);
|
|
89
|
+
return Array.isArray(raw) ? camelizeEach(raw) : raw;
|
|
90
|
+
}
|
|
91
|
+
async function applyEffects(ctx, def, result) {
|
|
92
|
+
if (def.clearSession === true) ctx.store.setData(null);
|
|
93
|
+
if (def.parseSession === true && def.skipStore !== true) {
|
|
94
|
+
if (result !== null && typeof result === "object" && "user" in result) ctx.store.setData(result);
|
|
95
|
+
}
|
|
96
|
+
if (def.refetchSession === true) await ctx.store.refetch();
|
|
97
|
+
}
|
|
98
|
+
function makeHttpRunner(ctx, def, boundInput) {
|
|
99
|
+
const run = (override) => runHttp(ctx, def, override === void 0 ? boundInput : override);
|
|
100
|
+
return run;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Execute a route's behaviour: delegate to its `handler` when present (handler
|
|
104
|
+
* owns all behaviour, including any effects), otherwise run the default
|
|
105
|
+
* pipeline and apply declarative effects once at the top level.
|
|
106
|
+
*/
|
|
107
|
+
async function dispatchRoute(ctx, def, input) {
|
|
108
|
+
if (def.handler !== void 0) return def.handler(ctx, input, makeHttpRunner(ctx, def, input));
|
|
109
|
+
const result = await runHttp(ctx, def, input);
|
|
110
|
+
await applyEffects(ctx, def, result);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Run a route as a public client call, firing the per-call `onSuccess` /
|
|
115
|
+
* `onError` hooks around the resolved value or thrown error.
|
|
116
|
+
*/
|
|
117
|
+
async function runRoute(ctx, def, input, opts) {
|
|
118
|
+
try {
|
|
119
|
+
const result = await dispatchRoute(ctx, def, input);
|
|
120
|
+
opts?.onSuccess?.(result);
|
|
121
|
+
return result;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
opts?.onError?.(error);
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/build-tree.ts
|
|
129
|
+
/** Scope a context's `fetch` to one plugin's base path (after client `overrides`). */
|
|
130
|
+
function scopeContext(ctx, fetcher, plugin, overrides) {
|
|
131
|
+
const defaultBase = normalizeBasePath(plugin.basePath ?? "");
|
|
132
|
+
const overrideBase = overrides?.[kebabToCamel(plugin.id)]?.basePath;
|
|
133
|
+
const resolvedBase = normalizeBasePath(overrideBase ?? plugin.basePath ?? "");
|
|
134
|
+
return {
|
|
135
|
+
...ctx,
|
|
136
|
+
fetch: (path, init) => {
|
|
137
|
+
const absolute = init?.absolute === true;
|
|
138
|
+
const requestPath = (absolute ? "" : resolvedBase) + ensureLeadingSlash(path);
|
|
139
|
+
const routePath = (absolute ? "" : defaultBase) + ensureLeadingSlash(path);
|
|
140
|
+
return fetcher.fetch(requestPath, init, routePath);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function chainFor(plugin, def) {
|
|
145
|
+
if (typeof def.as === "string") return chainFromDotted(def.as);
|
|
146
|
+
return [...pathToChain(plugin.basePath ?? ""), ...pathToChain(def.path)];
|
|
147
|
+
}
|
|
148
|
+
function mountAtChain(target, pathSegments, callable) {
|
|
149
|
+
let current = target;
|
|
150
|
+
for (let i = 0; i < pathSegments.length - 1; i += 1) {
|
|
151
|
+
const segment = pathSegments[i];
|
|
152
|
+
const child = current[segment];
|
|
153
|
+
if (child === void 0) {
|
|
154
|
+
const namespace = {};
|
|
155
|
+
current[segment] = namespace;
|
|
156
|
+
current = namespace;
|
|
157
|
+
} else current = child;
|
|
158
|
+
}
|
|
159
|
+
const finalSegment = pathSegments[pathSegments.length - 1];
|
|
160
|
+
current[finalSegment] = callable;
|
|
161
|
+
}
|
|
162
|
+
function isNamespace(value) {
|
|
163
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value !== "function";
|
|
164
|
+
}
|
|
165
|
+
function mergeInto(target, source) {
|
|
166
|
+
for (const [key, value] of Object.entries(source)) {
|
|
167
|
+
const existing = target[key];
|
|
168
|
+
if (isNamespace(existing) && isNamespace(value)) {
|
|
169
|
+
mergeInto(existing, value);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
target[key] = value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Build the public API object from plugin routes and actions.
|
|
177
|
+
*/
|
|
178
|
+
function buildClientTree({ plugins, ctx, fetcher, overrides }) {
|
|
179
|
+
const api = {};
|
|
180
|
+
for (const plugin of plugins) {
|
|
181
|
+
const scopedCtx = scopeContext(ctx, fetcher, plugin, overrides);
|
|
182
|
+
const contribution = {};
|
|
183
|
+
for (const def of plugin.routes) {
|
|
184
|
+
if (def.expose === false) continue;
|
|
185
|
+
const call = (input, opts) => runRoute(scopedCtx, def, input, opts);
|
|
186
|
+
mountAtChain(contribution, chainFor(plugin, def), call);
|
|
187
|
+
}
|
|
188
|
+
if (plugin.actions !== void 0) {
|
|
189
|
+
const run = (route, input) => runRoute(scopedCtx, route, input);
|
|
190
|
+
mergeInto(contribution, plugin.actions(scopedCtx, run));
|
|
191
|
+
}
|
|
192
|
+
mergeInto(api, contribution);
|
|
193
|
+
}
|
|
194
|
+
return api;
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/envelope.ts
|
|
198
|
+
/**
|
|
199
|
+
* Unwrap a parsed JSON success body according to envelope config.
|
|
200
|
+
*
|
|
201
|
+
* - `mode: "off"`: return as-is.
|
|
202
|
+
* - `mode: "wrap-success" | "always"`: extract `body[fields.data]` if present;
|
|
203
|
+
* if the key is missing (server didn't wrap this particular response), fall
|
|
204
|
+
* back to the raw body so we don't lose data.
|
|
205
|
+
*
|
|
206
|
+
* Returning `unknown` is intentional: the caller knows the expected shape and
|
|
207
|
+
* narrows / validates it. Trying to be clever here would invite false typing.
|
|
208
|
+
*/
|
|
209
|
+
function unwrapPayload(body, envelope) {
|
|
210
|
+
if (envelope.mode === "off") return body;
|
|
211
|
+
if (body === null || typeof body !== "object") return body;
|
|
212
|
+
const fields = envelope.fields ?? DEFAULT_ENVELOPE_FIELDS;
|
|
213
|
+
const record = body;
|
|
214
|
+
if (fields.data in record) return record[fields.data];
|
|
215
|
+
return body;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Pull the human-readable error message out of a non-2xx body.
|
|
219
|
+
*
|
|
220
|
+
* - `mode: "off" | "wrap-success"`: error bodies look like `{ message }`
|
|
221
|
+
* - `mode: "always"`: error bodies use the configured `fields.message` key.
|
|
222
|
+
*
|
|
223
|
+
* Returns `undefined` when no message can be located; the caller substitutes
|
|
224
|
+
* a status-based fallback (e.g. HTTP status text).
|
|
225
|
+
*/
|
|
226
|
+
function unwrapErrorMessage(body, envelope) {
|
|
227
|
+
if (body === null || typeof body !== "object") return;
|
|
228
|
+
const record = body;
|
|
229
|
+
const fields = envelope.fields ?? DEFAULT_ENVELOPE_FIELDS;
|
|
230
|
+
const value = record[envelope.mode === "always" ? fields.message : "message"];
|
|
231
|
+
return typeof value === "string" ? value : void 0;
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/fetcher.ts
|
|
235
|
+
var Fetcher = class {
|
|
236
|
+
opts;
|
|
237
|
+
fetchImpl;
|
|
238
|
+
credentials;
|
|
239
|
+
constructor(opts) {
|
|
240
|
+
this.opts = opts;
|
|
241
|
+
this.fetchImpl = opts.fetchOptions.impl ?? globalThis.fetch.bind(globalThis);
|
|
242
|
+
this.credentials = opts.fetchOptions.credentials ?? "include";
|
|
243
|
+
}
|
|
244
|
+
async fetch(path, init, routePath = path) {
|
|
245
|
+
const method = init?.method ?? (init?.body !== void 0 ? "POST" : "GET");
|
|
246
|
+
return this.run({
|
|
247
|
+
method,
|
|
248
|
+
path,
|
|
249
|
+
routePath,
|
|
250
|
+
body: init?.body,
|
|
251
|
+
headers: init?.headers,
|
|
252
|
+
query: init?.query
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Builds the request context, runs hooks, does
|
|
257
|
+
* the fetch, parses the response, runs hooks again, throws on non-2xx.
|
|
258
|
+
*/
|
|
259
|
+
async run(args) {
|
|
260
|
+
const fullPath = this.normalizeRelativePath(this.opts.basePath, args.path, args.query);
|
|
261
|
+
const url = joinURL(this.opts.baseURL, fullPath);
|
|
262
|
+
const headers = new Headers({
|
|
263
|
+
...args.headers,
|
|
264
|
+
...this.opts.fetchOptions.headers ?? {}
|
|
265
|
+
});
|
|
266
|
+
if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
|
|
267
|
+
if (!headers.has("Accept")) headers.set("Accept", "application/json");
|
|
268
|
+
let reqCtx = {
|
|
269
|
+
method: args.method,
|
|
270
|
+
fullPath,
|
|
271
|
+
path: args.path,
|
|
272
|
+
routePath: args.routePath,
|
|
273
|
+
url,
|
|
274
|
+
headers,
|
|
275
|
+
body: args.body
|
|
276
|
+
};
|
|
277
|
+
reqCtx = await this.opts.hooks.runBeforeRequest(reqCtx);
|
|
278
|
+
const payload = reqCtx.body !== void 0 && reqCtx.body !== null ? JSON.stringify(reqCtx.body) : void 0;
|
|
279
|
+
const requestInit = {
|
|
280
|
+
method: reqCtx.method,
|
|
281
|
+
headers: reqCtx.headers,
|
|
282
|
+
credentials: this.credentials
|
|
283
|
+
};
|
|
284
|
+
if (payload !== void 0) requestInit.body = payload;
|
|
285
|
+
let response;
|
|
286
|
+
try {
|
|
287
|
+
response = await this.fetchImpl(reqCtx.url, requestInit);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
this.opts.fetchOptions.onError?.({
|
|
290
|
+
...reqCtx,
|
|
291
|
+
status: 0,
|
|
292
|
+
ok: false,
|
|
293
|
+
error: err
|
|
294
|
+
});
|
|
295
|
+
throw new LimenError(err instanceof Error ? err.message : "Network request failed", 0, "unknown");
|
|
296
|
+
}
|
|
297
|
+
const parsedBody = await this.parseResponseBody(response);
|
|
298
|
+
const unwrapped = response.ok && parsedBody !== void 0 ? unwrapPayload(parsedBody, this.opts.envelope) : parsedBody;
|
|
299
|
+
let resCtx = {
|
|
300
|
+
method: args.method,
|
|
301
|
+
fullPath,
|
|
302
|
+
path: args.path,
|
|
303
|
+
routePath: args.routePath,
|
|
304
|
+
status: response.status,
|
|
305
|
+
ok: response.ok,
|
|
306
|
+
headers: response.headers,
|
|
307
|
+
body: unwrapped
|
|
308
|
+
};
|
|
309
|
+
resCtx = await this.opts.hooks.runAfterResponse(resCtx);
|
|
310
|
+
if (resCtx.ok) {
|
|
311
|
+
this.opts.fetchOptions.onSuccess?.({
|
|
312
|
+
...resCtx,
|
|
313
|
+
response
|
|
314
|
+
});
|
|
315
|
+
return resCtx.body;
|
|
316
|
+
}
|
|
317
|
+
const error = new LimenError(unwrapErrorMessage(resCtx.body, this.opts.envelope) ?? response.statusText ?? `Request failed with status ${response.status}`, response.status, deriveErrorCode(response.status));
|
|
318
|
+
this.opts.fetchOptions.onError?.({
|
|
319
|
+
...reqCtx,
|
|
320
|
+
status: response.status,
|
|
321
|
+
ok: false,
|
|
322
|
+
error
|
|
323
|
+
});
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
async parseResponseBody(response) {
|
|
327
|
+
if (response.status === 204) return;
|
|
328
|
+
const text = await response.text();
|
|
329
|
+
if (text.length === 0) return;
|
|
330
|
+
return JSON.parse(text);
|
|
331
|
+
}
|
|
332
|
+
normalizeRelativePath(basePath, relativePath, query) {
|
|
333
|
+
let path = (basePath === "" || basePath === "/" ? "" : ensureLeadingSlash(stripTrailingSlash(basePath))) + ensureLeadingSlash(relativePath);
|
|
334
|
+
if (query !== void 0) {
|
|
335
|
+
const qs = new URLSearchParams(query).toString();
|
|
336
|
+
if (qs.length > 0) path = `${path}?${qs}`;
|
|
337
|
+
}
|
|
338
|
+
return path;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/hooks.ts
|
|
343
|
+
function matchesRoute(matcher, routePath) {
|
|
344
|
+
if (matcher === void 0) return true;
|
|
345
|
+
if (typeof matcher === "string") return routePath === matcher;
|
|
346
|
+
if (typeof matcher === "function") return matcher({ path: routePath });
|
|
347
|
+
return routePath !== void 0 && matcher.includes(routePath);
|
|
348
|
+
}
|
|
349
|
+
var HookRunner = class {
|
|
350
|
+
before;
|
|
351
|
+
after;
|
|
352
|
+
constructor(plugins) {
|
|
353
|
+
this.before = plugins.flatMap((p) => p.hooks?.beforeRequest ?? []);
|
|
354
|
+
this.after = plugins.flatMap((p) => p.hooks?.afterResponse ?? []);
|
|
355
|
+
}
|
|
356
|
+
async runBeforeRequest(initial) {
|
|
357
|
+
let ctx = initial;
|
|
358
|
+
for (const hook of this.before) {
|
|
359
|
+
if (!matchesRoute(hook.match, ctx.routePath)) continue;
|
|
360
|
+
ctx = await hook.run(ctx);
|
|
361
|
+
}
|
|
362
|
+
return ctx;
|
|
363
|
+
}
|
|
364
|
+
async runAfterResponse(initial) {
|
|
365
|
+
let ctx = initial;
|
|
366
|
+
for (const hook of this.after) {
|
|
367
|
+
if (!matchesRoute(hook.match, ctx.routePath)) continue;
|
|
368
|
+
if (!hook.allowOnFailure && ctx.status >= 400) continue;
|
|
369
|
+
ctx = await hook.run(ctx);
|
|
370
|
+
}
|
|
371
|
+
return ctx;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/normalize.ts
|
|
376
|
+
/**
|
|
377
|
+
* Convert snake_case User keys from server payloads to camelCase. Unknown
|
|
378
|
+
* extension fields are converted with the same rule so consumers consistently
|
|
379
|
+
* read camelCase in SDK responses.
|
|
380
|
+
*/
|
|
381
|
+
function normalizeUser(raw) {
|
|
382
|
+
const out = {};
|
|
383
|
+
for (const [key, value] of Object.entries(raw)) out[toCamelCaseKey(key)] = value;
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
function defaultSessionParse(raw) {
|
|
387
|
+
if (!raw || typeof raw !== "object") throw new TypeError(`Expected session response to be an object, got ${raw === null ? "null" : typeof raw}`);
|
|
388
|
+
const obj = raw;
|
|
389
|
+
return { user: normalizeUser(obj["user"] ?? obj) };
|
|
390
|
+
}
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/routes.ts
|
|
393
|
+
/**
|
|
394
|
+
* Core routes available on every client.
|
|
395
|
+
*/
|
|
396
|
+
function coreClientPlugin() {
|
|
397
|
+
return defineClientPlugin({
|
|
398
|
+
id: "core",
|
|
399
|
+
basePath: "/",
|
|
400
|
+
routes: defineRoutes(route()({
|
|
401
|
+
method: "GET",
|
|
402
|
+
path: "/sessions"
|
|
403
|
+
}), route()({
|
|
404
|
+
method: "POST",
|
|
405
|
+
path: "/signout",
|
|
406
|
+
clearSession: true
|
|
407
|
+
}), route()({
|
|
408
|
+
method: "POST",
|
|
409
|
+
path: "/revoke-sessions",
|
|
410
|
+
clearSession: true
|
|
411
|
+
}), route()({
|
|
412
|
+
method: "POST",
|
|
413
|
+
path: "/verify-email",
|
|
414
|
+
refetchSession: true
|
|
415
|
+
}), route()({
|
|
416
|
+
method: "POST",
|
|
417
|
+
path: "/email-verifications",
|
|
418
|
+
as: "requestEmailVerification"
|
|
419
|
+
})),
|
|
420
|
+
actions: (ctx) => ({
|
|
421
|
+
/**
|
|
422
|
+
* Revalidate session state with `GET /me`, update `$session`, and return the
|
|
423
|
+
* resolved value (`null` when signed out).
|
|
424
|
+
*
|
|
425
|
+
* Prefer subscribing to `$session` for reactive UI state (`data`, `isPending`,
|
|
426
|
+
* `error`). Use `getSession()` when you need an awaited server re-check, such
|
|
427
|
+
* as route guards or SSR revalidation after `initialSession`.
|
|
428
|
+
*/
|
|
429
|
+
getSession: async () => {
|
|
430
|
+
await ctx.refetchSession();
|
|
431
|
+
const state = ctx.store.$session.get();
|
|
432
|
+
if (state.error) throw state.error;
|
|
433
|
+
return state.data;
|
|
434
|
+
} })
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Fetch and parse the current session for the reactive store.
|
|
439
|
+
*/
|
|
440
|
+
function createSessionHydrator(ctx) {
|
|
441
|
+
return async () => {
|
|
442
|
+
const raw = await ctx.fetch("/me", { method: "GET" });
|
|
443
|
+
return ctx.parseSession(raw);
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/broadcast-channel.ts
|
|
448
|
+
const NOOP_PORT = {
|
|
449
|
+
post() {},
|
|
450
|
+
subscribe: () => () => {},
|
|
451
|
+
close() {}
|
|
452
|
+
};
|
|
453
|
+
function createBroadcastChannel(name) {
|
|
454
|
+
if (typeof window === "undefined") return NOOP_PORT;
|
|
455
|
+
const storageKey = `${name}-sync`;
|
|
456
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
457
|
+
const emit = (message) => {
|
|
458
|
+
for (const listener of listeners) listener(message);
|
|
459
|
+
};
|
|
460
|
+
const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(name) : null;
|
|
461
|
+
if (channel) channel.onmessage = (event) => {
|
|
462
|
+
if (event.data != null) emit(event.data);
|
|
463
|
+
};
|
|
464
|
+
const onStorage = (event) => {
|
|
465
|
+
if (event.key !== storageKey || !event.newValue) return;
|
|
466
|
+
emit(JSON.parse(event.newValue));
|
|
467
|
+
};
|
|
468
|
+
if (!channel) window.addEventListener("storage", onStorage);
|
|
469
|
+
return {
|
|
470
|
+
post(message) {
|
|
471
|
+
if (channel) {
|
|
472
|
+
channel.postMessage(message);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (typeof globalThis.localStorage !== "undefined") {
|
|
476
|
+
globalThis.localStorage.setItem(storageKey, JSON.stringify(message));
|
|
477
|
+
globalThis.localStorage.removeItem(storageKey);
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
subscribe(listener) {
|
|
481
|
+
listeners.add(listener);
|
|
482
|
+
return () => {
|
|
483
|
+
listeners.delete(listener);
|
|
484
|
+
};
|
|
485
|
+
},
|
|
486
|
+
close() {
|
|
487
|
+
listeners.clear();
|
|
488
|
+
if (channel) {
|
|
489
|
+
channel.close();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
window.removeEventListener("storage", onStorage);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
//#endregion
|
|
497
|
+
//#region src/json-deep-equal.ts
|
|
498
|
+
function isPlainObject(value) {
|
|
499
|
+
if (typeof value !== "object" || value === null) return false;
|
|
500
|
+
const proto = Object.getPrototypeOf(value);
|
|
501
|
+
return proto === Object.prototype || proto === null;
|
|
502
|
+
}
|
|
503
|
+
function equalArrays(a, b) {
|
|
504
|
+
if (a.length !== b.length) return false;
|
|
505
|
+
for (let i = 0; i < a.length; i += 1) if (!deepJsonEqual(a[i], b[i])) return false;
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
function equalObjects(a, b) {
|
|
509
|
+
const aKeys = Object.keys(a);
|
|
510
|
+
const bKeys = Object.keys(b);
|
|
511
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
512
|
+
for (const key of aKeys) if (!Object.hasOwn(b, key) || !deepJsonEqual(a[key], b[key])) return false;
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
/** Structural equality for JSON-shaped values. */
|
|
516
|
+
function deepJsonEqual(a, b) {
|
|
517
|
+
if (Object.is(a, b)) return true;
|
|
518
|
+
if (Array.isArray(a) && Array.isArray(b)) return equalArrays(a, b);
|
|
519
|
+
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
|
520
|
+
return equalObjects(a, b);
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/session-sync.ts
|
|
524
|
+
const CHANNEL_NAME = "limen.session";
|
|
525
|
+
const FOCUS_REFETCH_THROTTLE_MS = 5e3;
|
|
526
|
+
function createSessionSync(store, options) {
|
|
527
|
+
if (options.fetchOnMount) store.refetch();
|
|
528
|
+
const teardowns = [];
|
|
529
|
+
if (options.crossTabSync) teardowns.push(syncAcrossTabs(store));
|
|
530
|
+
if (options.refetchOnWindowFocus) teardowns.push(refetchOnFocus(store));
|
|
531
|
+
return () => {
|
|
532
|
+
for (const teardown of teardowns) teardown();
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function syncAcrossTabs(store) {
|
|
536
|
+
const port = createBroadcastChannel(CHANNEL_NAME);
|
|
537
|
+
let lastData = store.$session.get().data;
|
|
538
|
+
const unsubscribe = port.subscribe((message) => {
|
|
539
|
+
lastData = message.data;
|
|
540
|
+
store.setData(message.data);
|
|
541
|
+
});
|
|
542
|
+
const unbindNotify = onNotify(store.$session, () => {
|
|
543
|
+
const data = store.$session.get().data;
|
|
544
|
+
if (deepJsonEqual(data, lastData)) return;
|
|
545
|
+
lastData = data;
|
|
546
|
+
port.post({ data });
|
|
547
|
+
});
|
|
548
|
+
return () => {
|
|
549
|
+
unbindNotify();
|
|
550
|
+
unsubscribe();
|
|
551
|
+
port.close();
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function refetchOnFocus(store) {
|
|
555
|
+
if (typeof document === "undefined") return () => {};
|
|
556
|
+
const onVisibilityChange = () => {
|
|
557
|
+
if (document.visibilityState === "visible") store.refetch({
|
|
558
|
+
maxAgeMs: FOCUS_REFETCH_THROTTLE_MS,
|
|
559
|
+
skipSignedOut: true
|
|
560
|
+
});
|
|
561
|
+
};
|
|
562
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
563
|
+
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
564
|
+
}
|
|
565
|
+
//#endregion
|
|
566
|
+
//#region src/session-store.ts
|
|
567
|
+
function createSessionStore(options) {
|
|
568
|
+
const $session = atom({
|
|
569
|
+
data: options.initialSession ?? null,
|
|
570
|
+
isPending: false,
|
|
571
|
+
error: null
|
|
572
|
+
});
|
|
573
|
+
let inFlightHydration = null;
|
|
574
|
+
let writeVersion = 0;
|
|
575
|
+
let lastRefreshedAt = 0;
|
|
576
|
+
const isStale = (requestVersion) => requestVersion !== writeVersion;
|
|
577
|
+
const fetchSessionFromServer = async () => {
|
|
578
|
+
const requestVersion = ++writeVersion;
|
|
579
|
+
$session.set({
|
|
580
|
+
data: $session.get().data,
|
|
581
|
+
isPending: true,
|
|
582
|
+
error: null
|
|
583
|
+
});
|
|
584
|
+
try {
|
|
585
|
+
const session = await options.hydrator();
|
|
586
|
+
if (isStale(requestVersion)) return;
|
|
587
|
+
$session.set({
|
|
588
|
+
data: session,
|
|
589
|
+
isPending: false,
|
|
590
|
+
error: null
|
|
591
|
+
});
|
|
592
|
+
} catch (err) {
|
|
593
|
+
if (isStale(requestVersion)) return;
|
|
594
|
+
if (err instanceof LimenError && err.isUnauthorized) {
|
|
595
|
+
$session.set({
|
|
596
|
+
data: null,
|
|
597
|
+
isPending: false,
|
|
598
|
+
error: null
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const error = err instanceof LimenError ? err : new LimenError(err instanceof Error ? err.message : "Failed to load session", 0, "unknown");
|
|
603
|
+
$session.set({
|
|
604
|
+
data: $session.get().data,
|
|
605
|
+
isPending: false,
|
|
606
|
+
error
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
const refetch = (options) => {
|
|
611
|
+
const { skipSignedOut, maxAgeMs } = options ?? {};
|
|
612
|
+
if (skipSignedOut && $session.get().data === null) return Promise.resolve();
|
|
613
|
+
if (maxAgeMs !== void 0 && Date.now() - lastRefreshedAt < maxAgeMs) return Promise.resolve();
|
|
614
|
+
if (!inFlightHydration) inFlightHydration = fetchSessionFromServer().finally(() => {
|
|
615
|
+
inFlightHydration = null;
|
|
616
|
+
lastRefreshedAt = Date.now();
|
|
617
|
+
});
|
|
618
|
+
return inFlightHydration;
|
|
619
|
+
};
|
|
620
|
+
const setData = (session) => {
|
|
621
|
+
writeVersion++;
|
|
622
|
+
$session.set({
|
|
623
|
+
data: session,
|
|
624
|
+
isPending: false,
|
|
625
|
+
error: null
|
|
626
|
+
});
|
|
627
|
+
};
|
|
628
|
+
const store = {
|
|
629
|
+
$session,
|
|
630
|
+
setData,
|
|
631
|
+
refetch
|
|
632
|
+
};
|
|
633
|
+
onMount($session, () => createSessionSync(store, {
|
|
634
|
+
fetchOnMount: options.initialSession === void 0,
|
|
635
|
+
crossTabSync: options.crossTabSync ?? false,
|
|
636
|
+
refetchOnWindowFocus: options.refetchOnWindowFocus ?? false
|
|
637
|
+
}));
|
|
638
|
+
return store;
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/client.ts
|
|
642
|
+
function createAuthClient(opts) {
|
|
643
|
+
const baseURL = stripTrailingSlash(opts.baseURL);
|
|
644
|
+
const basePath = normalizeBasePath(opts.basePath ?? "/auth");
|
|
645
|
+
const userPlugins = opts.plugins ?? [];
|
|
646
|
+
const plugins = [coreClientPlugin(), ...userPlugins];
|
|
647
|
+
const hooks = new HookRunner(plugins);
|
|
648
|
+
const fetcher = buildFetcher(baseURL, basePath, {
|
|
649
|
+
...DEFAULT_ENVELOPE_CONFIG,
|
|
650
|
+
...opts.envelope
|
|
651
|
+
}, hooks, opts.fetchOptions ?? {});
|
|
652
|
+
const parseSession = opts.parseSession ?? defaultSessionParse;
|
|
653
|
+
const redirect = resolveRedirect(opts.redirectFn);
|
|
654
|
+
const baseFetch = (path, init) => fetcher.fetch(path, init);
|
|
655
|
+
const store = createSessionStore({
|
|
656
|
+
hydrator: createSessionHydrator({
|
|
657
|
+
fetch: baseFetch,
|
|
658
|
+
parseSession
|
|
659
|
+
}),
|
|
660
|
+
crossTabSync: opts.crossTabSync !== false,
|
|
661
|
+
refetchOnWindowFocus: opts.refetchOnWindowFocus !== false,
|
|
662
|
+
...opts.initialSession !== void 0 ? { initialSession: opts.initialSession } : {}
|
|
663
|
+
});
|
|
664
|
+
return {
|
|
665
|
+
baseURL,
|
|
666
|
+
basePath,
|
|
667
|
+
...buildClientTree({
|
|
668
|
+
plugins,
|
|
669
|
+
ctx: {
|
|
670
|
+
fetch: baseFetch,
|
|
671
|
+
redirect,
|
|
672
|
+
parseSession,
|
|
673
|
+
setSession: (session) => store.setData(session),
|
|
674
|
+
refetchSession: () => store.refetch(),
|
|
675
|
+
store
|
|
676
|
+
},
|
|
677
|
+
fetcher,
|
|
678
|
+
overrides: opts.overrides
|
|
679
|
+
}),
|
|
680
|
+
$session: store.$session
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function buildFetcher(baseURL, basePath, envelope, hooks, fetchOptions) {
|
|
684
|
+
return new Fetcher({
|
|
685
|
+
baseURL,
|
|
686
|
+
basePath,
|
|
687
|
+
envelope,
|
|
688
|
+
hooks,
|
|
689
|
+
fetchOptions
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
function resolveRedirect(redirect) {
|
|
693
|
+
return (url) => {
|
|
694
|
+
if (redirect !== void 0) return redirect(url);
|
|
695
|
+
if (typeof window !== "undefined" && typeof window.location !== "undefined") {
|
|
696
|
+
window.location.href = url;
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
return false;
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
703
|
+
export { defaultSerialize as a, normalizeUser as i, coreClientPlugin as n, defaultSessionParse as r, createAuthClient as t };
|
|
704
|
+
|
|
705
|
+
//# sourceMappingURL=client-Er91De-z.mjs.map
|