@lunora/auth 1.0.0-alpha.13 → 1.0.0-alpha.130
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.md +38 -0
- package/README.md +99 -10
- package/dist/adapter.d.mts +4 -43
- package/dist/adapter.d.ts +4 -43
- package/dist/adapter.mjs +1 -48
- package/dist/audit.d.mts +131 -0
- package/dist/audit.d.ts +131 -0
- package/dist/audit.mjs +12 -0
- package/dist/email-guard.d.mts +127 -0
- package/dist/email-guard.d.ts +127 -0
- package/dist/email-guard.mjs +1 -0
- package/dist/index.d.mts +801 -157
- package/dist/index.d.ts +801 -157
- package/dist/index.mjs +1 -12
- package/dist/middleware.d.mts +167 -156
- package/dist/middleware.d.ts +167 -156
- package/dist/middleware.mjs +1 -53
- package/dist/packem_shared/AUTH_DO_AUDIT_PATH-ucS0M60j.mjs +1 -0
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DneiLGpv.mjs +1 -0
- package/dist/packem_shared/LunoraAuthAdminError-4r29Kj_y.mjs +1 -0
- package/dist/packem_shared/adapter-DA8DdALX.mjs +1 -0
- package/dist/packem_shared/adapter.d-2a61HAY2.d.ts +76 -0
- package/dist/packem_shared/adapter.d-DrD3bb1u.d.mts +76 -0
- package/dist/packem_shared/authAuditHook-C9Gt2iYp.mjs +1 -0
- package/dist/packem_shared/authDoColumnAdditions-UwF3Rl41.mjs +1 -0
- package/dist/packem_shared/compileMigrationsSql-ByVJhjR6.mjs +1 -0
- package/dist/packem_shared/create-auth.d-D1CKOFWc.d.mts +142 -0
- package/dist/packem_shared/create-auth.d-D1CKOFWc.d.ts +142 -0
- package/dist/packem_shared/createAuth-8aUw8v8D.mjs +1 -0
- package/dist/packem_shared/createDoAuthWiring-68YJP-VM.mjs +1 -0
- package/dist/packem_shared/createLunoraAuthClient-DhjhCwmM.mjs +1 -0
- package/dist/packem_shared/createSignUpInvitation-ClvskwuB.mjs +1 -0
- package/dist/packem_shared/emailGateDatabaseHooks-CAygR3iq.mjs +1 -0
- package/dist/packem_shared/invite-only.d-CsPJWAwM.d.ts +126 -0
- package/dist/packem_shared/invite-only.d-DKyHHkkQ.d.mts +126 -0
- package/dist/packem_shared/legacyIssuerCleanupStatements-D9Kf8OD1.mjs +1 -0
- package/dist/packem_shared/on-cloudflare-edge-Dx6gFjBO.mjs +1 -0
- package/dist/packem_shared/quote-identifier-CGiYFBvY.mjs +1 -0
- package/dist/packem_shared/sessionPresets-C867Mlo4.mjs +1 -0
- package/dist/packem_shared/uiConfig-B8pN6pks.mjs +1 -0
- package/dist/plugins-client.d.mts +165 -1
- package/dist/plugins-client.d.ts +165 -1
- package/dist/plugins-client.mjs +1 -2
- package/dist/plugins-enterprise-client.d.mts +15 -0
- package/dist/plugins-enterprise-client.d.ts +15 -0
- package/dist/plugins-enterprise-client.mjs +1 -0
- package/dist/plugins-enterprise.d.mts +1670 -0
- package/dist/plugins-enterprise.d.ts +1670 -0
- package/dist/plugins-enterprise.mjs +1 -0
- package/dist/plugins.d.mts +125 -2
- package/dist/plugins.d.ts +125 -2
- package/dist/plugins.mjs +1 -22
- package/dist/schema.d.mts +39 -39
- package/dist/schema.d.ts +39 -39
- package/dist/schema.mjs +1 -62
- package/dist/sql-store.d.mts +28 -28
- package/dist/sql-store.d.ts +28 -28
- package/dist/sql-store.mjs +1 -184
- package/dist/store.d.mts +65 -47
- package/dist/store.d.ts +65 -47
- package/dist/store.mjs +1 -183
- package/dist/turnstile-middleware.d.mts +66 -55
- package/dist/turnstile-middleware.d.ts +66 -55
- package/dist/turnstile-middleware.mjs +1 -45
- package/dist/turnstile.d.mts +42 -59
- package/dist/turnstile.d.ts +42 -59
- package/dist/turnstile.mjs +1 -61
- package/package.json +39 -5
- package/dist/packem_shared/DEFAULT_AUTH_BASE_PATH-DjcUWEQl.mjs +0 -11
- package/dist/packem_shared/LunoraAuthAdminError-BxrfEeA_.mjs +0 -249
- package/dist/packem_shared/compileMigrationsSql-cyE5U18R.mjs +0 -29
- package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.mts +0 -128
- package/dist/packem_shared/create-auth.d-Mwhb4gSc.d.ts +0 -128
- package/dist/packem_shared/createAuth-hRGxUJ9w.mjs +0 -85
- package/dist/packem_shared/sessionPresets-Dwwd74_J.mjs +0 -38
package/dist/turnstile.d.mts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cloudflare Turnstile server-side verification.
|
|
3
|
-
*
|
|
4
|
-
* Turnstile has **no Cloudflare binding** — verification is a single HTTPS POST
|
|
5
|
-
* to the public `siteverify` endpoint with your secret key. The secret lives in
|
|
6
|
-
* a plain env var / `.dev.vars` (conventionally `TURNSTILE_SECRET_KEY`), not in
|
|
7
|
-
* `wrangler.jsonc`. This module is therefore pure, transport-agnostic, and
|
|
8
|
-
* usable from any mutation/action — not just the auth flow.
|
|
9
|
-
* @see https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
|
|
10
|
-
*/
|
|
11
1
|
/** Cloudflare's public Turnstile `siteverify` endpoint. */
|
|
12
2
|
declare const TURNSTILE_VERIFY_ENDPOINT = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
|
13
3
|
/** Minimal `fetch` shape we depend on, so callers can inject a stub in tests. */
|
|
@@ -18,32 +8,32 @@ type FetchLike = (input: string, init?: {
|
|
|
18
8
|
}) => Promise<Response>;
|
|
19
9
|
interface VerifyTurnstileOptions {
|
|
20
10
|
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
11
|
+
* Assert the widget `action` the token was solved for. Cloudflare echoes the
|
|
12
|
+
* customer-supplied `action` back in the siteverify response; when this is
|
|
13
|
+
* set and the returned `action` does not match, the verdict is downgraded to
|
|
14
|
+
* `success: false` (with error code `action-mismatch`). Leave unset to skip
|
|
15
|
+
* the check.
|
|
16
|
+
*/
|
|
27
17
|
expectedAction?: string;
|
|
28
18
|
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
* Assert the `hostname` the challenge was solved on. Cloudflare returns the
|
|
20
|
+
* solving hostname in the siteverify response; when this is set and the
|
|
21
|
+
* returned `hostname` does not match, the verdict is downgraded to
|
|
22
|
+
* `success: false` (with error code `hostname-mismatch`). Set this when a
|
|
23
|
+
* single secret/sitekey is shared across multiple domains to stop a token
|
|
24
|
+
* harvested on one origin from being replayed against another. Leave unset
|
|
25
|
+
* to skip the check.
|
|
26
|
+
*/
|
|
37
27
|
expectedHostname?: string;
|
|
38
28
|
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
* Inject a `fetch` implementation. Defaults to `globalThis.fetch`. Primarily
|
|
30
|
+
* for unit tests — production callers can omit it.
|
|
31
|
+
*/
|
|
42
32
|
fetch?: FetchLike;
|
|
43
33
|
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
* The visitor's IP address, if known. Optional; Cloudflare uses it as an
|
|
35
|
+
* extra signal but verification works without it.
|
|
36
|
+
*/
|
|
47
37
|
remoteip?: string;
|
|
48
38
|
/** Your Turnstile secret key (the `TURNSTILE_SECRET_KEY` env var). */
|
|
49
39
|
secret: string;
|
|
@@ -51,13 +41,13 @@ interface VerifyTurnstileOptions {
|
|
|
51
41
|
token: string;
|
|
52
42
|
}
|
|
53
43
|
/**
|
|
54
|
-
* The normalized result of a Turnstile verification. Snake-cased fields from
|
|
55
|
-
* Cloudflare (`error-codes`, `challenge_ts`) are mapped to camelCase.
|
|
56
|
-
*
|
|
57
|
-
* A `success: false` verdict is a **bot/invalid-token** outcome, not an error —
|
|
58
|
-
* it is returned, never thrown. `verifyTurnstile` only throws on transport
|
|
59
|
-
* failure (network error, non-2xx response).
|
|
60
|
-
*/
|
|
44
|
+
* The normalized result of a Turnstile verification. Snake-cased fields from
|
|
45
|
+
* Cloudflare (`error-codes`, `challenge_ts`) are mapped to camelCase.
|
|
46
|
+
*
|
|
47
|
+
* A `success: false` verdict is a **bot/invalid-token** outcome, not an error —
|
|
48
|
+
* it is returned, never thrown. `verifyTurnstile` only throws on transport
|
|
49
|
+
* failure (network error, non-2xx response).
|
|
50
|
+
*/
|
|
61
51
|
interface TurnstileVerifyResult {
|
|
62
52
|
/** The customer-supplied `action` the widget was rendered with, if any. */
|
|
63
53
|
action?: string;
|
|
@@ -66,9 +56,9 @@ interface TurnstileVerifyResult {
|
|
|
66
56
|
/** ISO timestamp of the challenge, if Cloudflare returned one. */
|
|
67
57
|
challengeTs?: string;
|
|
68
58
|
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
* Cloudflare error codes for a failed verification (e.g.
|
|
60
|
+
* `"invalid-input-response"`, `"timeout-or-duplicate"`). Empty on success.
|
|
61
|
+
*/
|
|
72
62
|
errorCodes: string[];
|
|
73
63
|
/** Hostname the challenge was solved on, if Cloudflare returned one. */
|
|
74
64
|
hostname?: string;
|
|
@@ -76,23 +66,16 @@ interface TurnstileVerifyResult {
|
|
|
76
66
|
success: boolean;
|
|
77
67
|
}
|
|
78
68
|
/**
|
|
79
|
-
* Verify a Turnstile token against Cloudflare's `siteverify` endpoint.
|
|
80
|
-
*
|
|
81
|
-
* POSTs `application/x-www-form-urlencoded` (`secret`, `response`=token, and an
|
|
82
|
-
* optional `remoteip`) and returns the parsed verdict. A `success: false`
|
|
83
|
-
* outcome (bot / invalid / expired token) is **returned**, not thrown — callers
|
|
84
|
-
* decide how to react. The function throws a structural `LunoraError`-shaped
|
|
85
|
-
* error (`{ name: "LunoraError", code: "SERVICE_UNAVAILABLE", status: 503 }`)
|
|
86
|
-
* only when the siteverify call itself fails (network error or non-2xx), so a
|
|
87
|
-
* "siteverify is down" failure is distinguishable from a "this is a bot"
|
|
88
|
-
* verdict.
|
|
89
|
-
*/
|
|
90
|
-
declare const verifyTurnstile: ({
|
|
91
|
-
expectedAction,
|
|
92
|
-
expectedHostname,
|
|
93
|
-
fetch,
|
|
94
|
-
remoteip,
|
|
95
|
-
secret,
|
|
96
|
-
token
|
|
97
|
-
}: VerifyTurnstileOptions) => Promise<TurnstileVerifyResult>;
|
|
69
|
+
* Verify a Turnstile token against Cloudflare's `siteverify` endpoint.
|
|
70
|
+
*
|
|
71
|
+
* POSTs `application/x-www-form-urlencoded` (`secret`, `response`=token, and an
|
|
72
|
+
* optional `remoteip`) and returns the parsed verdict. A `success: false`
|
|
73
|
+
* outcome (bot / invalid / expired token) is **returned**, not thrown — callers
|
|
74
|
+
* decide how to react. The function throws a structural `LunoraError`-shaped
|
|
75
|
+
* error (`{ name: "LunoraError", code: "SERVICE_UNAVAILABLE", status: 503 }`)
|
|
76
|
+
* only when the siteverify call itself fails (network error or non-2xx), so a
|
|
77
|
+
* "siteverify is down" failure is distinguishable from a "this is a bot"
|
|
78
|
+
* verdict.
|
|
79
|
+
*/
|
|
80
|
+
declare const verifyTurnstile: ({ expectedAction, expectedHostname, fetch, remoteip, secret, token }: VerifyTurnstileOptions) => Promise<TurnstileVerifyResult>;
|
|
98
81
|
export { type FetchLike, TURNSTILE_VERIFY_ENDPOINT, type TurnstileVerifyResult, type VerifyTurnstileOptions, verifyTurnstile };
|
package/dist/turnstile.d.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cloudflare Turnstile server-side verification.
|
|
3
|
-
*
|
|
4
|
-
* Turnstile has **no Cloudflare binding** — verification is a single HTTPS POST
|
|
5
|
-
* to the public `siteverify` endpoint with your secret key. The secret lives in
|
|
6
|
-
* a plain env var / `.dev.vars` (conventionally `TURNSTILE_SECRET_KEY`), not in
|
|
7
|
-
* `wrangler.jsonc`. This module is therefore pure, transport-agnostic, and
|
|
8
|
-
* usable from any mutation/action — not just the auth flow.
|
|
9
|
-
* @see https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
|
|
10
|
-
*/
|
|
11
1
|
/** Cloudflare's public Turnstile `siteverify` endpoint. */
|
|
12
2
|
declare const TURNSTILE_VERIFY_ENDPOINT = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
|
|
13
3
|
/** Minimal `fetch` shape we depend on, so callers can inject a stub in tests. */
|
|
@@ -18,32 +8,32 @@ type FetchLike = (input: string, init?: {
|
|
|
18
8
|
}) => Promise<Response>;
|
|
19
9
|
interface VerifyTurnstileOptions {
|
|
20
10
|
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
11
|
+
* Assert the widget `action` the token was solved for. Cloudflare echoes the
|
|
12
|
+
* customer-supplied `action` back in the siteverify response; when this is
|
|
13
|
+
* set and the returned `action` does not match, the verdict is downgraded to
|
|
14
|
+
* `success: false` (with error code `action-mismatch`). Leave unset to skip
|
|
15
|
+
* the check.
|
|
16
|
+
*/
|
|
27
17
|
expectedAction?: string;
|
|
28
18
|
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
* Assert the `hostname` the challenge was solved on. Cloudflare returns the
|
|
20
|
+
* solving hostname in the siteverify response; when this is set and the
|
|
21
|
+
* returned `hostname` does not match, the verdict is downgraded to
|
|
22
|
+
* `success: false` (with error code `hostname-mismatch`). Set this when a
|
|
23
|
+
* single secret/sitekey is shared across multiple domains to stop a token
|
|
24
|
+
* harvested on one origin from being replayed against another. Leave unset
|
|
25
|
+
* to skip the check.
|
|
26
|
+
*/
|
|
37
27
|
expectedHostname?: string;
|
|
38
28
|
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
* Inject a `fetch` implementation. Defaults to `globalThis.fetch`. Primarily
|
|
30
|
+
* for unit tests — production callers can omit it.
|
|
31
|
+
*/
|
|
42
32
|
fetch?: FetchLike;
|
|
43
33
|
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
* The visitor's IP address, if known. Optional; Cloudflare uses it as an
|
|
35
|
+
* extra signal but verification works without it.
|
|
36
|
+
*/
|
|
47
37
|
remoteip?: string;
|
|
48
38
|
/** Your Turnstile secret key (the `TURNSTILE_SECRET_KEY` env var). */
|
|
49
39
|
secret: string;
|
|
@@ -51,13 +41,13 @@ interface VerifyTurnstileOptions {
|
|
|
51
41
|
token: string;
|
|
52
42
|
}
|
|
53
43
|
/**
|
|
54
|
-
* The normalized result of a Turnstile verification. Snake-cased fields from
|
|
55
|
-
* Cloudflare (`error-codes`, `challenge_ts`) are mapped to camelCase.
|
|
56
|
-
*
|
|
57
|
-
* A `success: false` verdict is a **bot/invalid-token** outcome, not an error —
|
|
58
|
-
* it is returned, never thrown. `verifyTurnstile` only throws on transport
|
|
59
|
-
* failure (network error, non-2xx response).
|
|
60
|
-
*/
|
|
44
|
+
* The normalized result of a Turnstile verification. Snake-cased fields from
|
|
45
|
+
* Cloudflare (`error-codes`, `challenge_ts`) are mapped to camelCase.
|
|
46
|
+
*
|
|
47
|
+
* A `success: false` verdict is a **bot/invalid-token** outcome, not an error —
|
|
48
|
+
* it is returned, never thrown. `verifyTurnstile` only throws on transport
|
|
49
|
+
* failure (network error, non-2xx response).
|
|
50
|
+
*/
|
|
61
51
|
interface TurnstileVerifyResult {
|
|
62
52
|
/** The customer-supplied `action` the widget was rendered with, if any. */
|
|
63
53
|
action?: string;
|
|
@@ -66,9 +56,9 @@ interface TurnstileVerifyResult {
|
|
|
66
56
|
/** ISO timestamp of the challenge, if Cloudflare returned one. */
|
|
67
57
|
challengeTs?: string;
|
|
68
58
|
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
* Cloudflare error codes for a failed verification (e.g.
|
|
60
|
+
* `"invalid-input-response"`, `"timeout-or-duplicate"`). Empty on success.
|
|
61
|
+
*/
|
|
72
62
|
errorCodes: string[];
|
|
73
63
|
/** Hostname the challenge was solved on, if Cloudflare returned one. */
|
|
74
64
|
hostname?: string;
|
|
@@ -76,23 +66,16 @@ interface TurnstileVerifyResult {
|
|
|
76
66
|
success: boolean;
|
|
77
67
|
}
|
|
78
68
|
/**
|
|
79
|
-
* Verify a Turnstile token against Cloudflare's `siteverify` endpoint.
|
|
80
|
-
*
|
|
81
|
-
* POSTs `application/x-www-form-urlencoded` (`secret`, `response`=token, and an
|
|
82
|
-
* optional `remoteip`) and returns the parsed verdict. A `success: false`
|
|
83
|
-
* outcome (bot / invalid / expired token) is **returned**, not thrown — callers
|
|
84
|
-
* decide how to react. The function throws a structural `LunoraError`-shaped
|
|
85
|
-
* error (`{ name: "LunoraError", code: "SERVICE_UNAVAILABLE", status: 503 }`)
|
|
86
|
-
* only when the siteverify call itself fails (network error or non-2xx), so a
|
|
87
|
-
* "siteverify is down" failure is distinguishable from a "this is a bot"
|
|
88
|
-
* verdict.
|
|
89
|
-
*/
|
|
90
|
-
declare const verifyTurnstile: ({
|
|
91
|
-
expectedAction,
|
|
92
|
-
expectedHostname,
|
|
93
|
-
fetch,
|
|
94
|
-
remoteip,
|
|
95
|
-
secret,
|
|
96
|
-
token
|
|
97
|
-
}: VerifyTurnstileOptions) => Promise<TurnstileVerifyResult>;
|
|
69
|
+
* Verify a Turnstile token against Cloudflare's `siteverify` endpoint.
|
|
70
|
+
*
|
|
71
|
+
* POSTs `application/x-www-form-urlencoded` (`secret`, `response`=token, and an
|
|
72
|
+
* optional `remoteip`) and returns the parsed verdict. A `success: false`
|
|
73
|
+
* outcome (bot / invalid / expired token) is **returned**, not thrown — callers
|
|
74
|
+
* decide how to react. The function throws a structural `LunoraError`-shaped
|
|
75
|
+
* error (`{ name: "LunoraError", code: "SERVICE_UNAVAILABLE", status: 503 }`)
|
|
76
|
+
* only when the siteverify call itself fails (network error or non-2xx), so a
|
|
77
|
+
* "siteverify is down" failure is distinguishable from a "this is a bot"
|
|
78
|
+
* verdict.
|
|
79
|
+
*/
|
|
80
|
+
declare const verifyTurnstile: ({ expectedAction, expectedHostname, fetch, remoteip, secret, token }: VerifyTurnstileOptions) => Promise<TurnstileVerifyResult>;
|
|
98
81
|
export { type FetchLike, TURNSTILE_VERIFY_ENDPOINT, type TurnstileVerifyResult, type VerifyTurnstileOptions, verifyTurnstile };
|
package/dist/turnstile.mjs
CHANGED
|
@@ -1,61 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
const asString = (value) => typeof value === "string" ? value : void 0;
|
|
3
|
-
const asStringArray = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
4
|
-
const verifyTurnstile = async ({
|
|
5
|
-
expectedAction,
|
|
6
|
-
expectedHostname,
|
|
7
|
-
fetch = globalThis.fetch,
|
|
8
|
-
remoteip,
|
|
9
|
-
secret,
|
|
10
|
-
token
|
|
11
|
-
}) => {
|
|
12
|
-
const body = new URLSearchParams({ response: token, secret });
|
|
13
|
-
if (remoteip !== void 0 && remoteip !== "") {
|
|
14
|
-
body.set("remoteip", remoteip);
|
|
15
|
-
}
|
|
16
|
-
let response;
|
|
17
|
-
try {
|
|
18
|
-
response = await fetch(TURNSTILE_VERIFY_ENDPOINT, {
|
|
19
|
-
body: body.toString(),
|
|
20
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
21
|
-
method: "POST"
|
|
22
|
-
});
|
|
23
|
-
} catch (error) {
|
|
24
|
-
throw Object.assign(new Error("turnstile siteverify request failed"), {
|
|
25
|
-
cause: error,
|
|
26
|
-
code: "SERVICE_UNAVAILABLE",
|
|
27
|
-
name: "LunoraError",
|
|
28
|
-
status: 503
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
if (!response.ok) {
|
|
32
|
-
throw Object.assign(new Error(`turnstile siteverify returned ${String(response.status)}`), {
|
|
33
|
-
code: "SERVICE_UNAVAILABLE",
|
|
34
|
-
name: "LunoraError",
|
|
35
|
-
status: 503
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
const raw = await response.json();
|
|
39
|
-
const action = asString(raw.action);
|
|
40
|
-
const hostname = asString(raw.hostname);
|
|
41
|
-
const errorCodes = asStringArray(raw["error-codes"]);
|
|
42
|
-
let success = raw.success === true;
|
|
43
|
-
if (success && expectedHostname !== void 0 && hostname !== expectedHostname) {
|
|
44
|
-
success = false;
|
|
45
|
-
errorCodes.push("hostname-mismatch");
|
|
46
|
-
}
|
|
47
|
-
if (success && expectedAction !== void 0 && action !== expectedAction) {
|
|
48
|
-
success = false;
|
|
49
|
-
errorCodes.push("action-mismatch");
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
action,
|
|
53
|
-
cdata: asString(raw.cdata),
|
|
54
|
-
challengeTs: asString(raw.challenge_ts),
|
|
55
|
-
errorCodes,
|
|
56
|
-
hostname,
|
|
57
|
-
success
|
|
58
|
-
};
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export { TURNSTILE_VERIFY_ENDPOINT, verifyTurnstile };
|
|
1
|
+
import{LunoraError as l}from"@lunora/errors";const E="https://challenges.cloudflare.com/turnstile/v0/siteverify",a=t=>typeof t=="string"?t:void 0,m=t=>Array.isArray(t)?t.filter(e=>typeof e=="string"):[],S=async({expectedAction:t,expectedHostname:e,fetch:d=globalThis.fetch,remoteip:n,secret:y,token:w})=>{const h=new URLSearchParams({response:w,secret:y});n!==void 0&&n!==""&&h.set("remoteip",n);let o;try{o=await d(E,{body:h.toString(),headers:{"content-type":"application/x-www-form-urlencoded"},method:"POST"})}catch(c){throw new l("SERVICE_UNAVAILABLE","turnstile siteverify request failed",{cause:c,status:503})}if(!o.ok)throw new l("SERVICE_UNAVAILABLE",`turnstile siteverify returned ${String(o.status)}`,{status:503});let r;try{r=await o.json()}catch(c){throw new l("SERVICE_UNAVAILABLE","turnstile siteverify returned a non-JSON body",{cause:c,status:503})}const u=a(r.action),f=a(r.hostname),i=m(r["error-codes"]);let s=r.success===!0;return s&&e!==void 0&&f!==e&&(s=!1,i.push("hostname-mismatch")),s&&t!==void 0&&u!==t&&(s=!1,i.push("action-mismatch")),{action:u,cdata:a(r.cdata),challengeTs:a(r.challenge_ts),errorCodes:i,hostname:f,success:s}};export{E as TURNSTILE_VERIFY_ENDPOINT,S as verifyTurnstile};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/auth",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.130",
|
|
4
4
|
"description": "Auth for Lunora — a thin better-auth wrapper: email/password, OAuth, plugins, D1-backed",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"auth",
|
|
@@ -48,10 +48,26 @@
|
|
|
48
48
|
"types": "./dist/plugins-client.d.ts",
|
|
49
49
|
"import": "./dist/plugins-client.mjs"
|
|
50
50
|
},
|
|
51
|
+
"./plugins/enterprise": {
|
|
52
|
+
"types": "./dist/plugins-enterprise.d.ts",
|
|
53
|
+
"import": "./dist/plugins-enterprise.mjs"
|
|
54
|
+
},
|
|
55
|
+
"./plugins/enterprise/client": {
|
|
56
|
+
"types": "./dist/plugins-enterprise-client.d.ts",
|
|
57
|
+
"import": "./dist/plugins-enterprise-client.mjs"
|
|
58
|
+
},
|
|
51
59
|
"./middleware": {
|
|
52
60
|
"types": "./dist/middleware.d.ts",
|
|
53
61
|
"import": "./dist/middleware.mjs"
|
|
54
62
|
},
|
|
63
|
+
"./audit": {
|
|
64
|
+
"types": "./dist/audit.d.ts",
|
|
65
|
+
"import": "./dist/audit.mjs"
|
|
66
|
+
},
|
|
67
|
+
"./email-guard": {
|
|
68
|
+
"types": "./dist/email-guard.d.ts",
|
|
69
|
+
"import": "./dist/email-guard.mjs"
|
|
70
|
+
},
|
|
55
71
|
"./turnstile": {
|
|
56
72
|
"types": "./dist/turnstile.d.ts",
|
|
57
73
|
"import": "./dist/turnstile.mjs"
|
|
@@ -82,10 +98,28 @@
|
|
|
82
98
|
"access": "public"
|
|
83
99
|
},
|
|
84
100
|
"dependencies": {
|
|
85
|
-
"@better-auth/
|
|
86
|
-
"@
|
|
87
|
-
"@
|
|
88
|
-
"better-auth": "
|
|
101
|
+
"@better-auth/api-key": "1.7.3",
|
|
102
|
+
"@better-auth/mcp": "1.7.3",
|
|
103
|
+
"@better-auth/oauth-provider": "1.7.3",
|
|
104
|
+
"@better-auth/passkey": "1.7.3",
|
|
105
|
+
"@better-auth/scim": "1.7.3",
|
|
106
|
+
"@lunora/errors": "1.0.0-alpha.35",
|
|
107
|
+
"@lunora/values": "1.0.0-alpha.43",
|
|
108
|
+
"@visulima/disposable-email-domains": "1.1.1",
|
|
109
|
+
"@visulima/email-verifier": "1.0.9",
|
|
110
|
+
"@visulima/free-email-domains": "1.0.1",
|
|
111
|
+
"@visulima/redact": "5.0.0"
|
|
112
|
+
},
|
|
113
|
+
"peerDependencies": {
|
|
114
|
+
"@better-auth/core": ">=1.7.3 <2.0.0-0",
|
|
115
|
+
"@better-auth/sso": ">=1.7.3 <2.0.0-0",
|
|
116
|
+
"@lunora/server": ">=1.0.0-alpha.24 <2.0.0-0",
|
|
117
|
+
"better-auth": ">=1.7.3 <2.0.0-0"
|
|
118
|
+
},
|
|
119
|
+
"peerDependenciesMeta": {
|
|
120
|
+
"@better-auth/sso": {
|
|
121
|
+
"optional": true
|
|
122
|
+
}
|
|
89
123
|
},
|
|
90
124
|
"engines": {
|
|
91
125
|
"node": "^22.15.0 || >=24.11.0"
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
const DEFAULT_AUTH_BASE_PATH = "/api/auth";
|
|
2
|
-
const handleAuthRequest = async (auth, request, basePath = DEFAULT_AUTH_BASE_PATH) => {
|
|
3
|
-
const url = new URL(request.url);
|
|
4
|
-
const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
5
|
-
if (url.pathname !== base && !url.pathname.startsWith(`${base}/`)) {
|
|
6
|
-
return void 0;
|
|
7
|
-
}
|
|
8
|
-
return auth.handler(request);
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export { DEFAULT_AUTH_BASE_PATH, handleAuthRequest };
|
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
class LunoraAuthAdminError extends Error {
|
|
2
|
-
code;
|
|
3
|
-
constructor(message, code) {
|
|
4
|
-
super(message);
|
|
5
|
-
this.name = "LunoraAuthAdminError";
|
|
6
|
-
this.code = code;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
const DEFAULT_LIMIT = 50;
|
|
10
|
-
const MAX_LIMIT = 500;
|
|
11
|
-
const DEFAULT_IMPERSONATION_SECONDS = 3600;
|
|
12
|
-
const MAX_IMPERSONATION_SECONDS = DEFAULT_IMPERSONATION_SECONDS * 24;
|
|
13
|
-
const MAX_BAN_SECONDS = 100 * 365 * 24 * 60 * 60;
|
|
14
|
-
const SENSITIVE_FIELDS = /* @__PURE__ */ new Set(["accessToken", "backupCodes", "idToken", "password", "publicKey", "refreshToken", "secret", "token"]);
|
|
15
|
-
const clampLimit = (limit) => Math.min(Math.max(Math.trunc(limit ?? DEFAULT_LIMIT), 1), MAX_LIMIT);
|
|
16
|
-
const clampOffset = (offset) => Math.max(0, Math.trunc(offset ?? 0));
|
|
17
|
-
const normalizeRow = (row) => {
|
|
18
|
-
const out = {};
|
|
19
|
-
for (const [key, value] of Object.entries(row)) {
|
|
20
|
-
if (SENSITIVE_FIELDS.has(key)) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
out[key] = value instanceof Date ? value.getTime() : value;
|
|
24
|
-
}
|
|
25
|
-
return out;
|
|
26
|
-
};
|
|
27
|
-
const serializeRole = (role) => Array.isArray(role) ? role.join(",") : role;
|
|
28
|
-
const asAdminError = (error) => {
|
|
29
|
-
if (error instanceof LunoraAuthAdminError) {
|
|
30
|
-
return error;
|
|
31
|
-
}
|
|
32
|
-
const candidate = error;
|
|
33
|
-
const code = candidate?.body?.code ?? candidate?.code ?? "AUTH_ADMIN_ERROR";
|
|
34
|
-
const message = candidate?.body?.message ?? candidate?.message ?? "auth admin operation failed";
|
|
35
|
-
return new LunoraAuthAdminError(message, code);
|
|
36
|
-
};
|
|
37
|
-
const createAuthAdmin = (auth, options = {}) => {
|
|
38
|
-
const context = auth.$context;
|
|
39
|
-
const features = options.features ?? {};
|
|
40
|
-
const withContext = async (function_) => {
|
|
41
|
-
try {
|
|
42
|
-
return await function_(await context);
|
|
43
|
-
} catch (error) {
|
|
44
|
-
throw asAdminError(error);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
const toUser = (row) => normalizeRow(row);
|
|
48
|
-
const page = async (context_, model, options_) => {
|
|
49
|
-
const where = options_.where && options_.where.length > 0 ? options_.where : void 0;
|
|
50
|
-
const [rows, total] = await Promise.all([
|
|
51
|
-
context_.adapter.findMany({
|
|
52
|
-
limit: clampLimit(options_.limit),
|
|
53
|
-
model,
|
|
54
|
-
offset: clampOffset(options_.offset),
|
|
55
|
-
sortBy: options_.sortBy,
|
|
56
|
-
where
|
|
57
|
-
}),
|
|
58
|
-
context_.adapter.count({ model, where })
|
|
59
|
-
]);
|
|
60
|
-
return { rows: rows.map((row) => normalizeRow(row)), total };
|
|
61
|
-
};
|
|
62
|
-
return {
|
|
63
|
-
banUser: ({ expiresInSeconds, reason, userId }) => withContext(async (context_) => {
|
|
64
|
-
const seconds = typeof expiresInSeconds === "number" && Number.isFinite(expiresInSeconds) ? Math.min(Math.trunc(expiresInSeconds), MAX_BAN_SECONDS) : 0;
|
|
65
|
-
const banExpires = seconds > 0 ? new Date(Date.now() + seconds * 1e3) : void 0;
|
|
66
|
-
const user = await context_.internalAdapter.updateUser(userId, {
|
|
67
|
-
banExpires,
|
|
68
|
-
banned: true,
|
|
69
|
-
banReason: reason ?? "No reason"
|
|
70
|
-
});
|
|
71
|
-
await context_.internalAdapter.deleteUserSessions(userId);
|
|
72
|
-
return toUser(user);
|
|
73
|
-
}),
|
|
74
|
-
cancelInvitation: ({ invitationId }) => withContext(async (context_) => {
|
|
75
|
-
await context_.adapter.delete({ model: "invitation", where: [{ field: "id", value: invitationId }] });
|
|
76
|
-
}),
|
|
77
|
-
capabilities: () => withContext((context_) => {
|
|
78
|
-
const ids = new Set((context_.options.plugins ?? []).map((plugin) => plugin.id));
|
|
79
|
-
const has = (id) => ids.has(id);
|
|
80
|
-
return Promise.resolve({
|
|
81
|
-
accounts: features.accounts ?? true,
|
|
82
|
-
admin: features.admin ?? has("admin"),
|
|
83
|
-
organization: features.organization ?? has("organization"),
|
|
84
|
-
passkey: features.passkey ?? has("passkey"),
|
|
85
|
-
twoFactor: features.twoFactor ?? has("two-factor")
|
|
86
|
-
});
|
|
87
|
-
}),
|
|
88
|
-
// The one op that genuinely builds a row rather than mutating one. Replicates
|
|
89
|
-
// the plugin's create-user handler over `internalAdapter` (lowercase + dedupe
|
|
90
|
-
// email, create the row, then link a credential account when a password is given).
|
|
91
|
-
createUser: ({ data, email, name, password, role }) => withContext(async (context_) => {
|
|
92
|
-
const normalizedEmail = email.toLowerCase();
|
|
93
|
-
if (await context_.internalAdapter.findUserByEmail(normalizedEmail)) {
|
|
94
|
-
throw new LunoraAuthAdminError("a user with this email already exists", "USER_ALREADY_EXISTS");
|
|
95
|
-
}
|
|
96
|
-
const user = await context_.internalAdapter.createUser({
|
|
97
|
-
email: normalizedEmail,
|
|
98
|
-
name,
|
|
99
|
-
role: role === void 0 ? void 0 : serializeRole(role),
|
|
100
|
-
...data
|
|
101
|
-
});
|
|
102
|
-
if (password !== void 0 && password !== "") {
|
|
103
|
-
const hashed = await context_.password.hash(password);
|
|
104
|
-
await context_.internalAdapter.linkAccount({
|
|
105
|
-
accountId: user.id,
|
|
106
|
-
password: hashed,
|
|
107
|
-
providerId: "credential",
|
|
108
|
-
userId: user.id
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
return toUser(user);
|
|
112
|
-
}),
|
|
113
|
-
deletePasskey: ({ passkeyId }) => withContext(async (context_) => {
|
|
114
|
-
await context_.adapter.delete({ model: "passkey", where: [{ field: "id", value: passkeyId }] });
|
|
115
|
-
}),
|
|
116
|
-
disableTwoFactor: ({ userId }) => withContext(async (context_) => {
|
|
117
|
-
await context_.adapter.deleteMany({ model: "twoFactor", where: [{ field: "userId", value: userId }] });
|
|
118
|
-
await context_.internalAdapter.updateUser(userId, { twoFactorEnabled: false });
|
|
119
|
-
}),
|
|
120
|
-
impersonateUser: ({ userId }) => withContext(async (context_) => {
|
|
121
|
-
const user = await context_.internalAdapter.findUserById(userId);
|
|
122
|
-
if (!user) {
|
|
123
|
-
throw new LunoraAuthAdminError("user not found", "USER_NOT_FOUND");
|
|
124
|
-
}
|
|
125
|
-
const rawSeconds = options.impersonationSeconds;
|
|
126
|
-
let ttlSeconds = DEFAULT_IMPERSONATION_SECONDS;
|
|
127
|
-
if (rawSeconds !== void 0) {
|
|
128
|
-
if (!Number.isInteger(rawSeconds) || !Number.isFinite(rawSeconds) || rawSeconds <= 0) {
|
|
129
|
-
throw new LunoraAuthAdminError("impersonationSeconds must be a positive finite integer", "INVALID_IMPERSONATION_SECONDS");
|
|
130
|
-
}
|
|
131
|
-
ttlSeconds = Math.min(rawSeconds, MAX_IMPERSONATION_SECONDS);
|
|
132
|
-
}
|
|
133
|
-
const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
|
|
134
|
-
const session = await context_.internalAdapter.createSession(
|
|
135
|
-
userId,
|
|
136
|
-
true,
|
|
137
|
-
{ expiresAt, impersonatedBy: options.impersonatedBy ?? userId },
|
|
138
|
-
true
|
|
139
|
-
);
|
|
140
|
-
return {
|
|
141
|
-
expiresAt: session.expiresAt instanceof Date ? session.expiresAt.getTime() : expiresAt.getTime(),
|
|
142
|
-
token: session.token,
|
|
143
|
-
user: toUser(user)
|
|
144
|
-
};
|
|
145
|
-
}),
|
|
146
|
-
listAccounts: ({ userId }) => withContext(async (context_) => {
|
|
147
|
-
const rows = await context_.adapter.findMany({ model: "account", where: [{ field: "userId", value: userId }] });
|
|
148
|
-
return rows.map((row) => normalizeRow(row));
|
|
149
|
-
}),
|
|
150
|
-
listInvitations: ({ limit, offset, organizationId }) => withContext(
|
|
151
|
-
(context_) => page(context_, "invitation", {
|
|
152
|
-
limit,
|
|
153
|
-
offset,
|
|
154
|
-
where: [{ field: "organizationId", value: organizationId }]
|
|
155
|
-
})
|
|
156
|
-
),
|
|
157
|
-
listMembers: ({ limit, offset, organizationId }) => withContext(
|
|
158
|
-
(context_) => page(context_, "member", {
|
|
159
|
-
limit,
|
|
160
|
-
offset,
|
|
161
|
-
sortBy: { direction: "desc", field: "createdAt" },
|
|
162
|
-
where: [{ field: "organizationId", value: organizationId }]
|
|
163
|
-
})
|
|
164
|
-
),
|
|
165
|
-
listOrganizations: ({ limit, offset }) => withContext((context_) => page(context_, "organization", { limit, offset, sortBy: { direction: "desc", field: "createdAt" } })),
|
|
166
|
-
listPasskeys: ({ userId }) => withContext(async (context_) => {
|
|
167
|
-
const rows = await context_.adapter.findMany({ model: "passkey", where: [{ field: "userId", value: userId }] });
|
|
168
|
-
return rows.map((row) => normalizeRow(row));
|
|
169
|
-
}),
|
|
170
|
-
listSessions: ({ limit, offset, userId }) => withContext(
|
|
171
|
-
(context_) => page(context_, "session", {
|
|
172
|
-
limit,
|
|
173
|
-
offset,
|
|
174
|
-
sortBy: { direction: "desc", field: "createdAt" },
|
|
175
|
-
where: userId === void 0 || userId === "" ? void 0 : [{ field: "userId", value: userId }]
|
|
176
|
-
})
|
|
177
|
-
),
|
|
178
|
-
listUsers: ({ filterField, filterValue, limit, offset, search, searchField, sortBy, sortDirection }) => withContext((context_) => {
|
|
179
|
-
const where = [];
|
|
180
|
-
if (search !== void 0 && search !== "") {
|
|
181
|
-
where.push({ field: searchField ?? "email", operator: "contains", value: search });
|
|
182
|
-
}
|
|
183
|
-
if (filterValue !== void 0) {
|
|
184
|
-
where.push({ field: filterField ?? "email", operator: "eq", value: filterValue });
|
|
185
|
-
}
|
|
186
|
-
return page(context_, "user", {
|
|
187
|
-
limit,
|
|
188
|
-
offset,
|
|
189
|
-
sortBy: { direction: sortDirection ?? "desc", field: sortBy ?? "createdAt" },
|
|
190
|
-
where
|
|
191
|
-
});
|
|
192
|
-
}),
|
|
193
|
-
removeMember: ({ memberId }) => withContext(async (context_) => {
|
|
194
|
-
await context_.adapter.delete({ model: "member", where: [{ field: "id", value: memberId }] });
|
|
195
|
-
}),
|
|
196
|
-
removeUser: ({ userId }) => withContext(async (context_) => {
|
|
197
|
-
await context_.internalAdapter.deleteUserSessions(userId);
|
|
198
|
-
await context_.internalAdapter.deleteUser(userId);
|
|
199
|
-
}),
|
|
200
|
-
// Keyed on the session *id*, not its token: tokens are bearer credentials we
|
|
201
|
-
// deliberately never surface to the studio. Resolve the row to recover its
|
|
202
|
-
// token, then delete via `internalAdapter.deleteSession` — which also clears
|
|
203
|
-
// secondary (KV) storage, unlike a raw `adapter.delete` on the DB row.
|
|
204
|
-
revokeUserSession: ({ sessionId }) => withContext(async (context_) => {
|
|
205
|
-
const session = await context_.adapter.findOne({ model: "session", where: [{ field: "id", value: sessionId }] });
|
|
206
|
-
if (session?.token) {
|
|
207
|
-
await context_.internalAdapter.deleteSession(session.token);
|
|
208
|
-
}
|
|
209
|
-
}),
|
|
210
|
-
revokeUserSessions: ({ userId }) => withContext(async (context_) => {
|
|
211
|
-
await context_.internalAdapter.deleteUserSessions(userId);
|
|
212
|
-
}),
|
|
213
|
-
setRole: ({ role, userId }) => withContext(async (context_) => {
|
|
214
|
-
const user = await context_.internalAdapter.updateUser(userId, { role: serializeRole(role) });
|
|
215
|
-
return toUser(user);
|
|
216
|
-
}),
|
|
217
|
-
setUserPassword: ({ newPassword, userId }) => withContext(async (context_) => {
|
|
218
|
-
const min = context_.password.config.minPasswordLength;
|
|
219
|
-
const max = context_.password.config.maxPasswordLength;
|
|
220
|
-
if (newPassword.length < min) {
|
|
221
|
-
throw new LunoraAuthAdminError(`password must be at least ${min.toString()} characters`, "PASSWORD_TOO_SHORT");
|
|
222
|
-
}
|
|
223
|
-
if (newPassword.length > max) {
|
|
224
|
-
throw new LunoraAuthAdminError(`password must be at most ${max.toString()} characters`, "PASSWORD_TOO_LONG");
|
|
225
|
-
}
|
|
226
|
-
const hashed = await context_.password.hash(newPassword);
|
|
227
|
-
await context_.internalAdapter.updatePassword(userId, hashed);
|
|
228
|
-
}),
|
|
229
|
-
unbanUser: ({ userId }) => withContext(async (context_) => {
|
|
230
|
-
const user = await context_.internalAdapter.updateUser(userId, { banExpires: null, banned: false, banReason: null });
|
|
231
|
-
return toUser(user);
|
|
232
|
-
}),
|
|
233
|
-
unlinkAccount: ({ accountId, userId }) => withContext(async (context_) => {
|
|
234
|
-
await context_.adapter.delete({
|
|
235
|
-
model: "account",
|
|
236
|
-
where: [
|
|
237
|
-
{ field: "id", value: accountId },
|
|
238
|
-
{ connector: "AND", field: "userId", value: userId }
|
|
239
|
-
]
|
|
240
|
-
});
|
|
241
|
-
}),
|
|
242
|
-
updateUser: ({ data, userId }) => withContext(async (context_) => {
|
|
243
|
-
const user = await context_.internalAdapter.updateUser(userId, data);
|
|
244
|
-
return toUser(user);
|
|
245
|
-
})
|
|
246
|
-
};
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
export { LunoraAuthAdminError, createAuthAdmin };
|