@lunora/auth 1.0.0-alpha.14 → 1.0.0-alpha.140
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-th2-Hb30.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-CG0Yv85f.mjs +2 -0
- package/dist/packem_shared/adapter.d-BW_ovHul.d.mts +89 -0
- package/dist/packem_shared/adapter.d-BWw-tC_U.d.ts +89 -0
- package/dist/packem_shared/authAuditHook-C9Gt2iYp.mjs +1 -0
- package/dist/packem_shared/authDoColumnAdditions-UwF3Rl41.mjs +1 -0
- package/dist/packem_shared/compileMigrationsSql-Ds2snsWg.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-C8BRUjf6.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/sql-store-CYa4HJsO.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 +73 -55
- package/dist/turnstile-middleware.d.ts +73 -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/store.mjs
CHANGED
|
@@ -1,183 +1 @@
|
|
|
1
|
-
const
|
|
2
|
-
const isNullish = (value) => value === void 0 || value === null;
|
|
3
|
-
const looseEquals = (left, right, insensitive) => {
|
|
4
|
-
if (insensitive) {
|
|
5
|
-
const a = asString(left);
|
|
6
|
-
const b = asString(right);
|
|
7
|
-
if (a !== void 0 && b !== void 0) {
|
|
8
|
-
return a.toLowerCase() === b.toLowerCase();
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
return left === right;
|
|
12
|
-
};
|
|
13
|
-
const matchesPattern = (cell, value, operator, insensitive) => {
|
|
14
|
-
const haystack = asString(cell);
|
|
15
|
-
const needle = asString(value);
|
|
16
|
-
if (haystack === void 0 || needle === void 0) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
const [a, b] = insensitive ? [haystack.toLowerCase(), needle.toLowerCase()] : [haystack, needle];
|
|
20
|
-
if (operator === "contains") {
|
|
21
|
-
return a.includes(b);
|
|
22
|
-
}
|
|
23
|
-
return operator === "starts_with" ? a.startsWith(b) : a.endsWith(b);
|
|
24
|
-
};
|
|
25
|
-
const evaluateClause = (row, clause) => {
|
|
26
|
-
const { field, mode, operator, value } = clause;
|
|
27
|
-
const cell = row[field];
|
|
28
|
-
const insensitive = mode === "insensitive";
|
|
29
|
-
switch (operator) {
|
|
30
|
-
case "contains":
|
|
31
|
-
case "ends_with":
|
|
32
|
-
case "starts_with": {
|
|
33
|
-
return matchesPattern(cell, value, operator, insensitive);
|
|
34
|
-
}
|
|
35
|
-
case "gt": {
|
|
36
|
-
return !isNullish(value) && cell > value;
|
|
37
|
-
}
|
|
38
|
-
case "gte": {
|
|
39
|
-
return !isNullish(value) && cell >= value;
|
|
40
|
-
}
|
|
41
|
-
case "in": {
|
|
42
|
-
return Array.isArray(value) && value.some((entry) => looseEquals(cell, entry, insensitive));
|
|
43
|
-
}
|
|
44
|
-
case "lt": {
|
|
45
|
-
return !isNullish(value) && cell < value;
|
|
46
|
-
}
|
|
47
|
-
case "lte": {
|
|
48
|
-
return !isNullish(value) && cell <= value;
|
|
49
|
-
}
|
|
50
|
-
case "ne": {
|
|
51
|
-
return !looseEquals(cell, value, insensitive);
|
|
52
|
-
}
|
|
53
|
-
case "not_in": {
|
|
54
|
-
return Array.isArray(value) && !value.some((entry) => looseEquals(cell, entry, insensitive));
|
|
55
|
-
}
|
|
56
|
-
// "eq" and the default: null-aware equality.
|
|
57
|
-
default: {
|
|
58
|
-
return isNullish(value) ? isNullish(cell) : looseEquals(cell, value, insensitive);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
const compareCells = (a, b) => {
|
|
63
|
-
if (typeof a === "string" && typeof b === "string") {
|
|
64
|
-
return a.localeCompare(b);
|
|
65
|
-
}
|
|
66
|
-
const left = Number(a);
|
|
67
|
-
const right = Number(b);
|
|
68
|
-
if (left < right) {
|
|
69
|
-
return -1;
|
|
70
|
-
}
|
|
71
|
-
return left > right ? 1 : 0;
|
|
72
|
-
};
|
|
73
|
-
const sortRows = (rows, sortBy) => {
|
|
74
|
-
const direction = sortBy.direction === "asc" ? 1 : -1;
|
|
75
|
-
return rows.toSorted((left, right) => {
|
|
76
|
-
const a = left[sortBy.field];
|
|
77
|
-
const b = right[sortBy.field];
|
|
78
|
-
if (isNullish(a) && isNullish(b)) {
|
|
79
|
-
return 0;
|
|
80
|
-
}
|
|
81
|
-
if (isNullish(a)) {
|
|
82
|
-
return -direction;
|
|
83
|
-
}
|
|
84
|
-
if (isNullish(b)) {
|
|
85
|
-
return direction;
|
|
86
|
-
}
|
|
87
|
-
return compareCells(a, b) * direction;
|
|
88
|
-
});
|
|
89
|
-
};
|
|
90
|
-
const matchesWhere = (row, where) => {
|
|
91
|
-
let result = true;
|
|
92
|
-
for (const [index, clause] of where.entries()) {
|
|
93
|
-
const clauseResult = evaluateClause(row, clause);
|
|
94
|
-
if (index === 0) {
|
|
95
|
-
result = clauseResult;
|
|
96
|
-
} else if (clause.connector === "OR") {
|
|
97
|
-
result = result || clauseResult;
|
|
98
|
-
} else {
|
|
99
|
-
result = result && clauseResult;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return result;
|
|
103
|
-
};
|
|
104
|
-
const createMemoryAuthStore = () => {
|
|
105
|
-
const tables = /* @__PURE__ */ new Map();
|
|
106
|
-
const tableOf = (model) => {
|
|
107
|
-
const existing = tables.get(model);
|
|
108
|
-
if (existing) {
|
|
109
|
-
return existing;
|
|
110
|
-
}
|
|
111
|
-
const created = [];
|
|
112
|
-
tables.set(model, created);
|
|
113
|
-
return created;
|
|
114
|
-
};
|
|
115
|
-
return {
|
|
116
|
-
consumeOne: (model, where) => {
|
|
117
|
-
const table = tableOf(model);
|
|
118
|
-
const index = table.findIndex((row2) => matchesWhere(row2, where));
|
|
119
|
-
if (index === -1) {
|
|
120
|
-
return Promise.resolve(void 0);
|
|
121
|
-
}
|
|
122
|
-
const [row] = table.splice(index, 1);
|
|
123
|
-
return Promise.resolve(row ? { ...row } : void 0);
|
|
124
|
-
},
|
|
125
|
-
count: (model, where) => Promise.resolve(tableOf(model).filter((row) => matchesWhere(row, where)).length),
|
|
126
|
-
create: (model, data) => {
|
|
127
|
-
const row = { ...data };
|
|
128
|
-
tableOf(model).push(row);
|
|
129
|
-
return Promise.resolve({ ...row });
|
|
130
|
-
},
|
|
131
|
-
incrementOne: (model, where, increment, set) => {
|
|
132
|
-
const row = tableOf(model).find((candidate) => matchesWhere(candidate, where));
|
|
133
|
-
if (!row) {
|
|
134
|
-
return Promise.resolve(void 0);
|
|
135
|
-
}
|
|
136
|
-
for (const [field, delta] of Object.entries(increment)) {
|
|
137
|
-
row[field] = (typeof row[field] === "number" ? row[field] : 0) + delta;
|
|
138
|
-
}
|
|
139
|
-
if (set) {
|
|
140
|
-
Object.assign(row, set);
|
|
141
|
-
}
|
|
142
|
-
return Promise.resolve({ ...row });
|
|
143
|
-
},
|
|
144
|
-
read: (model, query) => {
|
|
145
|
-
let rows = tableOf(model).filter((row) => matchesWhere(row, query.where));
|
|
146
|
-
if (query.sortBy) {
|
|
147
|
-
rows = sortRows(rows, query.sortBy);
|
|
148
|
-
}
|
|
149
|
-
if (query.offset) {
|
|
150
|
-
rows = rows.slice(query.offset);
|
|
151
|
-
}
|
|
152
|
-
if (query.limit !== void 0) {
|
|
153
|
-
rows = rows.slice(0, query.limit);
|
|
154
|
-
}
|
|
155
|
-
return Promise.resolve(
|
|
156
|
-
rows.map((row) => {
|
|
157
|
-
return { ...row };
|
|
158
|
-
})
|
|
159
|
-
);
|
|
160
|
-
},
|
|
161
|
-
remove: (model, where) => {
|
|
162
|
-
const table = tableOf(model);
|
|
163
|
-
const kept = table.filter((row) => !matchesWhere(row, where));
|
|
164
|
-
const removed = table.length - kept.length;
|
|
165
|
-
table.length = 0;
|
|
166
|
-
table.push(...kept);
|
|
167
|
-
return Promise.resolve(removed);
|
|
168
|
-
},
|
|
169
|
-
update: (model, where, values) => {
|
|
170
|
-
const matched = tableOf(model).filter((row) => matchesWhere(row, where));
|
|
171
|
-
for (const row of matched) {
|
|
172
|
-
Object.assign(row, values);
|
|
173
|
-
}
|
|
174
|
-
return Promise.resolve(
|
|
175
|
-
matched.map((row) => {
|
|
176
|
-
return { ...row };
|
|
177
|
-
})
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
export { createMemoryAuthStore, matchesWhere };
|
|
1
|
+
const d=i=>typeof i=="string"?i:void 0,a=i=>i==null,f=(i,o,n)=>{if(n){const s=d(i),e=d(o);if(s!==void 0&&e!==void 0)return s.toLowerCase()===e.toLowerCase()}return i===o},v=(i,o,n,s)=>{const e=d(i),t=d(o);if(e===void 0||t===void 0)return!1;const[r,c]=s?[e.toLowerCase(),t.toLowerCase()]:[e,t];return n==="contains"?r.includes(c):n==="starts_with"?r.startsWith(c):r.endsWith(c)},m=(i,o)=>{const{field:n,mode:s,operator:e,value:t}=o,r=i[n],c=s==="insensitive";switch(e){case"contains":case"ends_with":case"starts_with":return v(r,t,e,c);case"gt":return!a(t)&&r>t;case"gte":return!a(t)&&r>=t;case"in":return Array.isArray(t)&&t.some(u=>f(r,u,c));case"lt":return!a(t)&&r<t;case"lte":return!a(t)&&r<=t;case"ne":return!f(r,t,c);case"not_in":return Array.isArray(t)&&!t.some(u=>f(r,u,c));default:return a(t)?a(r):f(r,t,c)}},h=(i,o)=>{if(typeof i=="string"&&typeof o=="string")return i.localeCompare(o);const n=Number(i),s=Number(o);return n<s?-1:n>s?1:0},g=(i,o)=>{const n=o.direction==="asc"?1:-1;return i.toSorted((s,e)=>{const t=s[o.field],r=e[o.field];return a(t)&&a(r)?0:a(t)?-n:a(r)?n:h(t,r)*n})},l=(i,o)=>{const n=o.filter(e=>e.connector==="OR");return o.filter(e=>e.connector!=="OR").every(e=>m(i,e))&&(n.length===0||n.some(e=>m(i,e)))},p=()=>{const i=new Map,o=n=>{const s=i.get(n);if(s)return s;const e=[];return i.set(n,e),e};return{consumeOne:(n,s)=>{const e=o(n),t=e.findIndex(c=>l(c,s));if(t===-1)return Promise.resolve(void 0);const[r]=e.splice(t,1);return Promise.resolve(r?{...r}:void 0)},count:(n,s)=>Promise.resolve(o(n).filter(e=>l(e,s)).length),create:(n,s)=>{const e={...s};return o(n).push(e),Promise.resolve({...e})},incrementOne:(n,s,e,t)=>{const r=o(n).find(c=>l(c,s));if(!r)return Promise.resolve(void 0);for(const[c,u]of Object.entries(e))r[c]=(typeof r[c]=="number"?r[c]:0)+u;return t&&Object.assign(r,t),Promise.resolve({...r})},read:(n,s)=>{let e=o(n).filter(t=>l(t,s.where));return s.sortBy&&(e=g(e,s.sortBy)),s.offset&&(e=e.slice(s.offset)),s.limit!==void 0&&(e=e.slice(0,s.limit)),Promise.resolve(e.map(t=>({...t})))},remove:(n,s)=>{const e=o(n),t=e.filter(c=>!l(c,s)),r=e.length-t.length;return e.length=0,e.push(...t),Promise.resolve(r)},update:(n,s,e)=>{const t=o(n).filter(r=>l(r,s));for(const r of t)Object.assign(r,e);return Promise.resolve(t.map(r=>({...r})))}}};export{p as createMemoryAuthStore,l as matchesWhere};
|
|
@@ -2,79 +2,97 @@ import { Middleware } from '@lunora/server';
|
|
|
2
2
|
import { FetchLike, TurnstileVerifyResult } from "./turnstile.mjs";
|
|
3
3
|
interface VerifyTurnstileMiddlewareOptions<Context> {
|
|
4
4
|
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
* Assert the widget `action` the token was solved for (forwarded to
|
|
6
|
+
* {@link verifyTurnstile}). When set and the siteverify response's `action`
|
|
7
|
+
* does not match, the verdict is treated as a failure (403). Optional.
|
|
8
|
+
*/
|
|
9
9
|
expectedAction?: string;
|
|
10
10
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
* Assert the `hostname` the challenge was solved on (forwarded to
|
|
12
|
+
* {@link verifyTurnstile}). When set and the siteverify response's `hostname`
|
|
13
|
+
* does not match, the verdict is treated as a failure (403). Set this when a
|
|
14
|
+
* single secret/sitekey is shared across multiple domains to stop a token
|
|
15
|
+
* harvested on one origin being replayed against this procedure. Optional.
|
|
16
|
+
*/
|
|
17
17
|
expectedHostname?: string;
|
|
18
18
|
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
* Behavior when the siteverify call itself throws (network error, non-2xx).
|
|
20
|
+
* Defaults to `false` (**fail closed**: reject with 403). Set `true` only
|
|
21
|
+
* when degraded availability is preferable to denying traffic — a failing
|
|
22
|
+
* siteverify then admits every request. Mirrors `@lunora/ratelimit`'s
|
|
23
|
+
* `rateLimit` failure policy.
|
|
24
|
+
*/
|
|
25
25
|
failOpen?: boolean;
|
|
26
26
|
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
* Inject a `fetch` implementation (forwarded to {@link verifyTurnstile}).
|
|
28
|
+
* Primarily for tests.
|
|
29
|
+
*/
|
|
30
30
|
fetch?: FetchLike;
|
|
31
31
|
/** Override the error message thrown on a failed verdict. */
|
|
32
32
|
message?: string;
|
|
33
33
|
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
* Selector that pulls the visitor IP off `ctx` — typically `ctx.ip`, or
|
|
35
|
+
* `ctx.args.ip` when the caller sends it (the procedure context has no raw
|
|
36
|
+
* `Headers`). Optional.
|
|
37
|
+
*/
|
|
37
38
|
remoteip?: (context: Context) => string | undefined;
|
|
38
39
|
/** Your Turnstile secret key (the `TURNSTILE_SECRET_KEY` env var). */
|
|
39
40
|
secret: string;
|
|
40
41
|
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
* Selector that pulls the `cf-turnstile-response` token off `ctx`. The
|
|
43
|
+
* procedure context carries only the resolved identity, **not** the raw
|
|
44
|
+
* inbound `Headers` (see `withAuthPlugins` in `./middleware`), so the token
|
|
45
|
+
* travels in the function `args` — which the builder surfaces to middleware
|
|
46
|
+
* as `ctx.args` (validated, frozen):
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* export const submit = mutation
|
|
50
|
+
* .input({ message: v.string(), turnstileToken: v.string() })
|
|
51
|
+
* .use(verifyTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, token: (ctx) => ctx.args.turnstileToken }))
|
|
52
|
+
* .mutation(async ({ args, ctx }) => { … });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
46
55
|
token: (context: Context) => string | undefined;
|
|
47
56
|
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
* Extra predicate run on a `success: true` verdict (after the built-in
|
|
58
|
+
* `expectedHostname`/`expectedAction` checks). Return `false` to reject the
|
|
59
|
+
* request with 403 — use it for any custom replay/abuse guard that needs the
|
|
60
|
+
* full verdict (e.g. matching `cdata`, or one hostname out of an allow-list).
|
|
61
|
+
*/
|
|
53
62
|
validate?: (result: TurnstileVerifyResult) => boolean;
|
|
54
63
|
}
|
|
55
64
|
/**
|
|
56
|
-
* Procedure middleware that enforces a Turnstile (CAPTCHA) check before the
|
|
57
|
-
* handler runs. Attach it with `.use()`. It reads the token (and optional IP)
|
|
58
|
-
* from `ctx` via the provided selectors — because the procedure context does
|
|
59
|
-
* not carry raw request headers, the token
|
|
60
|
-
* `args
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
65
|
+
* Procedure middleware that enforces a Turnstile (CAPTCHA) check before the
|
|
66
|
+
* handler runs. Attach it with `.use()`. It reads the token (and optional IP)
|
|
67
|
+
* from `ctx` via the provided selectors — because the procedure context does
|
|
68
|
+
* not carry raw request headers, the token is passed through the function
|
|
69
|
+
* `args` and read back as `ctx.args.<field>` (the builder surfaces the
|
|
70
|
+
* VALIDATED args on the middleware context, frozen). To gate the better-auth
|
|
71
|
+
* sign-in/sign-up flow itself, prefer
|
|
72
|
+
* better-auth's native `captcha` plugin (re-exported from `@lunora/auth/plugins`)
|
|
73
|
+
* — this middleware is for non-auth Lunora procedures.
|
|
74
|
+
*
|
|
75
|
+
* On a `success: false` verdict (or a missing token) it throws a structural
|
|
76
|
+
* `LunoraError` (`{ name: "LunoraError", code: "FORBIDDEN", status: 403 }`) —
|
|
77
|
+
* the runtime maps it to the matching RPC/HTTP status without any import of
|
|
78
|
+
* the error machinery from `@lunora/server`.
|
|
79
|
+
*
|
|
80
|
+
* The middleware is tagged PER-DISPATCH: a Turnstile token is single-use, so
|
|
81
|
+
* verifying one is an effect of the request that a memoized response cannot
|
|
82
|
+
* stand in for. The builder hoists the mark onto the registered function and
|
|
83
|
+
* the generated `isCacheableQuery` keeps such a query out of the reactive
|
|
84
|
+
* cache — a hit answers without running this chain, so one solved token would
|
|
85
|
+
* otherwise admit every request the memo served.
|
|
86
|
+
*
|
|
87
|
+
* **Failure policy:** if the siteverify call itself throws, the middleware
|
|
88
|
+
* **fails closed by default** (logs and rejects with 403). Pass
|
|
89
|
+
* `failOpen: true` to admit the request instead — mirrors `@lunora/ratelimit`.
|
|
90
|
+
*
|
|
91
|
+
* **Cross-origin replay:** a token solved on one origin can be replayed against
|
|
92
|
+
* a different endpoint when a single secret/sitekey is shared across multiple
|
|
93
|
+
* domains. Pass `expectedHostname` (and optionally `expectedAction`, or a custom
|
|
94
|
+
* `validate(result)` predicate) to assert the siteverify response's `hostname`
|
|
95
|
+
* /`action` and reject mismatches with 403.
|
|
96
|
+
*/
|
|
79
97
|
declare const verifyTurnstileMiddleware: <Context>(options: VerifyTurnstileMiddlewareOptions<Context>) => Middleware<Context, Context>;
|
|
80
98
|
export { type VerifyTurnstileMiddlewareOptions, verifyTurnstileMiddleware };
|
|
@@ -2,79 +2,97 @@ import { Middleware } from '@lunora/server';
|
|
|
2
2
|
import { FetchLike, TurnstileVerifyResult } from "./turnstile.js";
|
|
3
3
|
interface VerifyTurnstileMiddlewareOptions<Context> {
|
|
4
4
|
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
* Assert the widget `action` the token was solved for (forwarded to
|
|
6
|
+
* {@link verifyTurnstile}). When set and the siteverify response's `action`
|
|
7
|
+
* does not match, the verdict is treated as a failure (403). Optional.
|
|
8
|
+
*/
|
|
9
9
|
expectedAction?: string;
|
|
10
10
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
* Assert the `hostname` the challenge was solved on (forwarded to
|
|
12
|
+
* {@link verifyTurnstile}). When set and the siteverify response's `hostname`
|
|
13
|
+
* does not match, the verdict is treated as a failure (403). Set this when a
|
|
14
|
+
* single secret/sitekey is shared across multiple domains to stop a token
|
|
15
|
+
* harvested on one origin being replayed against this procedure. Optional.
|
|
16
|
+
*/
|
|
17
17
|
expectedHostname?: string;
|
|
18
18
|
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
* Behavior when the siteverify call itself throws (network error, non-2xx).
|
|
20
|
+
* Defaults to `false` (**fail closed**: reject with 403). Set `true` only
|
|
21
|
+
* when degraded availability is preferable to denying traffic — a failing
|
|
22
|
+
* siteverify then admits every request. Mirrors `@lunora/ratelimit`'s
|
|
23
|
+
* `rateLimit` failure policy.
|
|
24
|
+
*/
|
|
25
25
|
failOpen?: boolean;
|
|
26
26
|
/**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
* Inject a `fetch` implementation (forwarded to {@link verifyTurnstile}).
|
|
28
|
+
* Primarily for tests.
|
|
29
|
+
*/
|
|
30
30
|
fetch?: FetchLike;
|
|
31
31
|
/** Override the error message thrown on a failed verdict. */
|
|
32
32
|
message?: string;
|
|
33
33
|
/**
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
* Selector that pulls the visitor IP off `ctx` — typically `ctx.ip`, or
|
|
35
|
+
* `ctx.args.ip` when the caller sends it (the procedure context has no raw
|
|
36
|
+
* `Headers`). Optional.
|
|
37
|
+
*/
|
|
37
38
|
remoteip?: (context: Context) => string | undefined;
|
|
38
39
|
/** Your Turnstile secret key (the `TURNSTILE_SECRET_KEY` env var). */
|
|
39
40
|
secret: string;
|
|
40
41
|
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
* Selector that pulls the `cf-turnstile-response` token off `ctx`. The
|
|
43
|
+
* procedure context carries only the resolved identity, **not** the raw
|
|
44
|
+
* inbound `Headers` (see `withAuthPlugins` in `./middleware`), so the token
|
|
45
|
+
* travels in the function `args` — which the builder surfaces to middleware
|
|
46
|
+
* as `ctx.args` (validated, frozen):
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* export const submit = mutation
|
|
50
|
+
* .input({ message: v.string(), turnstileToken: v.string() })
|
|
51
|
+
* .use(verifyTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, token: (ctx) => ctx.args.turnstileToken }))
|
|
52
|
+
* .mutation(async ({ args, ctx }) => { … });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
46
55
|
token: (context: Context) => string | undefined;
|
|
47
56
|
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
* Extra predicate run on a `success: true` verdict (after the built-in
|
|
58
|
+
* `expectedHostname`/`expectedAction` checks). Return `false` to reject the
|
|
59
|
+
* request with 403 — use it for any custom replay/abuse guard that needs the
|
|
60
|
+
* full verdict (e.g. matching `cdata`, or one hostname out of an allow-list).
|
|
61
|
+
*/
|
|
53
62
|
validate?: (result: TurnstileVerifyResult) => boolean;
|
|
54
63
|
}
|
|
55
64
|
/**
|
|
56
|
-
* Procedure middleware that enforces a Turnstile (CAPTCHA) check before the
|
|
57
|
-
* handler runs. Attach it with `.use()`. It reads the token (and optional IP)
|
|
58
|
-
* from `ctx` via the provided selectors — because the procedure context does
|
|
59
|
-
* not carry raw request headers, the token
|
|
60
|
-
* `args
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
65
|
+
* Procedure middleware that enforces a Turnstile (CAPTCHA) check before the
|
|
66
|
+
* handler runs. Attach it with `.use()`. It reads the token (and optional IP)
|
|
67
|
+
* from `ctx` via the provided selectors — because the procedure context does
|
|
68
|
+
* not carry raw request headers, the token is passed through the function
|
|
69
|
+
* `args` and read back as `ctx.args.<field>` (the builder surfaces the
|
|
70
|
+
* VALIDATED args on the middleware context, frozen). To gate the better-auth
|
|
71
|
+
* sign-in/sign-up flow itself, prefer
|
|
72
|
+
* better-auth's native `captcha` plugin (re-exported from `@lunora/auth/plugins`)
|
|
73
|
+
* — this middleware is for non-auth Lunora procedures.
|
|
74
|
+
*
|
|
75
|
+
* On a `success: false` verdict (or a missing token) it throws a structural
|
|
76
|
+
* `LunoraError` (`{ name: "LunoraError", code: "FORBIDDEN", status: 403 }`) —
|
|
77
|
+
* the runtime maps it to the matching RPC/HTTP status without any import of
|
|
78
|
+
* the error machinery from `@lunora/server`.
|
|
79
|
+
*
|
|
80
|
+
* The middleware is tagged PER-DISPATCH: a Turnstile token is single-use, so
|
|
81
|
+
* verifying one is an effect of the request that a memoized response cannot
|
|
82
|
+
* stand in for. The builder hoists the mark onto the registered function and
|
|
83
|
+
* the generated `isCacheableQuery` keeps such a query out of the reactive
|
|
84
|
+
* cache — a hit answers without running this chain, so one solved token would
|
|
85
|
+
* otherwise admit every request the memo served.
|
|
86
|
+
*
|
|
87
|
+
* **Failure policy:** if the siteverify call itself throws, the middleware
|
|
88
|
+
* **fails closed by default** (logs and rejects with 403). Pass
|
|
89
|
+
* `failOpen: true` to admit the request instead — mirrors `@lunora/ratelimit`.
|
|
90
|
+
*
|
|
91
|
+
* **Cross-origin replay:** a token solved on one origin can be replayed against
|
|
92
|
+
* a different endpoint when a single secret/sitekey is shared across multiple
|
|
93
|
+
* domains. Pass `expectedHostname` (and optionally `expectedAction`, or a custom
|
|
94
|
+
* `validate(result)` predicate) to assert the siteverify response's `hostname`
|
|
95
|
+
* /`action` and reject mismatches with 403.
|
|
96
|
+
*/
|
|
79
97
|
declare const verifyTurnstileMiddleware: <Context>(options: VerifyTurnstileMiddlewareOptions<Context>) => Middleware<Context, Context>;
|
|
80
98
|
export { type VerifyTurnstileMiddlewareOptions, verifyTurnstileMiddleware };
|
|
@@ -1,45 +1 @@
|
|
|
1
|
-
import { verifyTurnstile }
|
|
2
|
-
|
|
3
|
-
const verifyTurnstileMiddleware = (options) => async ({ ctx, next }) => {
|
|
4
|
-
const token = options.token(ctx);
|
|
5
|
-
if (token === void 0 || token === "") {
|
|
6
|
-
throw Object.assign(new Error(options.message ?? "turnstile token missing"), {
|
|
7
|
-
code: "FORBIDDEN",
|
|
8
|
-
name: "LunoraError",
|
|
9
|
-
status: 403
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
let result;
|
|
13
|
-
try {
|
|
14
|
-
result = await verifyTurnstile({
|
|
15
|
-
expectedAction: options.expectedAction,
|
|
16
|
-
expectedHostname: options.expectedHostname,
|
|
17
|
-
fetch: options.fetch,
|
|
18
|
-
remoteip: options.remoteip?.(ctx),
|
|
19
|
-
secret: options.secret,
|
|
20
|
-
token
|
|
21
|
-
});
|
|
22
|
-
} catch (error) {
|
|
23
|
-
console.error(`@lunora/auth: verifyTurnstileMiddleware siteverify threw; ${options.failOpen ? "failing open" : "failing closed"}`, error);
|
|
24
|
-
if (options.failOpen) {
|
|
25
|
-
return next();
|
|
26
|
-
}
|
|
27
|
-
throw Object.assign(new Error(options.message ?? "turnstile verification unavailable"), {
|
|
28
|
-
cause: error,
|
|
29
|
-
code: "FORBIDDEN",
|
|
30
|
-
name: "LunoraError",
|
|
31
|
-
status: 403
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
if (!result.success || options.validate !== void 0 && !options.validate(result)) {
|
|
35
|
-
throw Object.assign(new Error(options.message ?? "turnstile verification failed"), {
|
|
36
|
-
code: "FORBIDDEN",
|
|
37
|
-
errorCodes: result.errorCodes,
|
|
38
|
-
name: "LunoraError",
|
|
39
|
-
status: 403
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return next();
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export { verifyTurnstileMiddleware };
|
|
1
|
+
import{LunoraError as i}from"@lunora/errors";import{tagPerDispatchMiddleware as d}from"@lunora/server";import{verifyTurnstile as n}from"./turnstile.mjs";const u=e=>d(async({ctx:a,next:l})=>{const t=e.token(a);if(t===void 0||t==="")throw new i("FORBIDDEN",e.message??"turnstile token missing");let r;try{r=await n({expectedAction:e.expectedAction,expectedHostname:e.expectedHostname,fetch:e.fetch,remoteip:e.remoteip?.(a),secret:e.secret,token:t})}catch(c){if(console.error(`@lunora/auth: verifyTurnstileMiddleware siteverify threw; ${e.failOpen?"failing open":"failing closed"}`,c),e.failOpen)return l();throw new i("FORBIDDEN",e.message??"turnstile verification unavailable",{cause:c})}if(!r.success||e.validate!==void 0&&e.validate(r)!==!0)throw new i("FORBIDDEN",e.message??"turnstile verification failed",{data:r.errorCodes.length>0?{errorCodes:r.errorCodes}:void 0});return l()});export{u as verifyTurnstileMiddleware};
|