@shaferllc/keel 0.59.0 → 0.66.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/dist/core/application.js +12 -0
- package/dist/core/auth.d.ts +47 -0
- package/dist/core/auth.js +77 -0
- package/dist/core/authorization.d.ts +9 -1
- package/dist/core/authorization.js +22 -2
- package/dist/core/container.d.ts +20 -0
- package/dist/core/container.js +52 -0
- package/dist/core/cors.d.ts +29 -0
- package/dist/core/cors.js +72 -0
- package/dist/core/crypto.d.ts +40 -4
- package/dist/core/crypto.js +66 -6
- package/dist/core/csrf.d.ts +25 -0
- package/dist/core/csrf.js +78 -0
- package/dist/core/database.d.ts +49 -4
- package/dist/core/database.js +89 -21
- package/dist/core/helpers.d.ts +6 -0
- package/dist/core/helpers.js +12 -0
- package/dist/core/index.d.ts +18 -8
- package/dist/core/index.js +9 -4
- package/dist/core/model.d.ts +2 -0
- package/dist/core/model.js +16 -14
- package/dist/core/provider.d.ts +7 -0
- package/dist/core/provider.js +7 -0
- package/dist/core/rate-limit.js +3 -0
- package/dist/core/relations.js +13 -13
- package/dist/core/request.d.ts +26 -0
- package/dist/core/request.js +77 -0
- package/dist/core/shield.d.ts +39 -0
- package/dist/core/shield.js +60 -0
- package/dist/core/social.d.ts +173 -0
- package/dist/core/social.js +337 -0
- package/dist/core/tokens.d.ts +74 -0
- package/dist/core/tokens.js +155 -0
- package/dist/db/d1.d.ts +32 -0
- package/dist/db/d1.js +26 -0
- package/dist/db/libsql.d.ts +29 -0
- package/dist/db/libsql.js +32 -0
- package/dist/db/pg.d.ts +29 -0
- package/dist/db/pg.js +33 -0
- package/package.json +13 -1
package/dist/core/relations.js
CHANGED
|
@@ -56,7 +56,7 @@ export class HasMany extends Relation {
|
|
|
56
56
|
return this.parent[this.localKey];
|
|
57
57
|
}
|
|
58
58
|
query() {
|
|
59
|
-
return db(this.related.table).where(this.foreignKey, this.localValue());
|
|
59
|
+
return db(this.related.table, this.related.connection).where(this.foreignKey, this.localValue());
|
|
60
60
|
}
|
|
61
61
|
async get() {
|
|
62
62
|
return this.hydrate(await this.query().get());
|
|
@@ -64,7 +64,7 @@ export class HasMany extends Relation {
|
|
|
64
64
|
async eager(models, name) {
|
|
65
65
|
const keys = unique(models.map((m) => m[this.localKey]).filter((v) => v != null));
|
|
66
66
|
const rows = keys.length
|
|
67
|
-
? await db(this.related.table).whereIn(this.foreignKey, keys).get()
|
|
67
|
+
? await db(this.related.table, this.related.connection).whereIn(this.foreignKey, keys).get()
|
|
68
68
|
: [];
|
|
69
69
|
const grouped = new Map();
|
|
70
70
|
for (const row of rows) {
|
|
@@ -90,7 +90,7 @@ export class HasOne extends Relation {
|
|
|
90
90
|
return this.parent[this.localKey];
|
|
91
91
|
}
|
|
92
92
|
query() {
|
|
93
|
-
return db(this.related.table).where(this.foreignKey, this.localValue());
|
|
93
|
+
return db(this.related.table, this.related.connection).where(this.foreignKey, this.localValue());
|
|
94
94
|
}
|
|
95
95
|
async get() {
|
|
96
96
|
const row = await this.query().first();
|
|
@@ -99,7 +99,7 @@ export class HasOne extends Relation {
|
|
|
99
99
|
async eager(models, name) {
|
|
100
100
|
const keys = unique(models.map((m) => m[this.localKey]).filter((v) => v != null));
|
|
101
101
|
const rows = keys.length
|
|
102
|
-
? await db(this.related.table).whereIn(this.foreignKey, keys).get()
|
|
102
|
+
? await db(this.related.table, this.related.connection).whereIn(this.foreignKey, keys).get()
|
|
103
103
|
: [];
|
|
104
104
|
const byKey = new Map();
|
|
105
105
|
for (const row of rows) {
|
|
@@ -124,7 +124,7 @@ export class BelongsTo extends Relation {
|
|
|
124
124
|
return this.parent[this.foreignKey];
|
|
125
125
|
}
|
|
126
126
|
query() {
|
|
127
|
-
return db(this.related.table).where(this.ownerKey, this.foreignValue());
|
|
127
|
+
return db(this.related.table, this.related.connection).where(this.ownerKey, this.foreignValue());
|
|
128
128
|
}
|
|
129
129
|
async get() {
|
|
130
130
|
if (this.foreignValue() == null)
|
|
@@ -135,7 +135,7 @@ export class BelongsTo extends Relation {
|
|
|
135
135
|
async eager(models, name) {
|
|
136
136
|
const keys = unique(models.map((m) => m[this.foreignKey]).filter((v) => v != null));
|
|
137
137
|
const rows = keys.length
|
|
138
|
-
? await db(this.related.table).whereIn(this.ownerKey, keys).get()
|
|
138
|
+
? await db(this.related.table, this.related.connection).whereIn(this.ownerKey, keys).get()
|
|
139
139
|
: [];
|
|
140
140
|
const byKey = new Map();
|
|
141
141
|
for (const row of rows)
|
|
@@ -165,18 +165,18 @@ export class BelongsToMany extends Relation {
|
|
|
165
165
|
}
|
|
166
166
|
/** The query against the related table, once pivot rows are known. */
|
|
167
167
|
query() {
|
|
168
|
-
return db(this.related.table);
|
|
168
|
+
return db(this.related.table, this.related.connection);
|
|
169
169
|
}
|
|
170
170
|
async get() {
|
|
171
171
|
if (this.parentValue() == null)
|
|
172
172
|
return [];
|
|
173
|
-
const pivots = await db(this.pivotTable)
|
|
173
|
+
const pivots = await db(this.pivotTable, this.related.connection)
|
|
174
174
|
.where(this.foreignPivotKey, this.parentValue())
|
|
175
175
|
.get();
|
|
176
176
|
const relatedIds = unique(pivots.map((p) => p[this.relatedPivotKey]));
|
|
177
177
|
if (!relatedIds.length)
|
|
178
178
|
return [];
|
|
179
|
-
const rows = await db(this.related.table).whereIn(this.relatedKey, relatedIds).get();
|
|
179
|
+
const rows = await db(this.related.table, this.related.connection).whereIn(this.relatedKey, relatedIds).get();
|
|
180
180
|
return this.hydrate(rows);
|
|
181
181
|
}
|
|
182
182
|
async eager(models, name) {
|
|
@@ -186,10 +186,10 @@ export class BelongsToMany extends Relation {
|
|
|
186
186
|
m.setRelation(name, []);
|
|
187
187
|
return;
|
|
188
188
|
}
|
|
189
|
-
const pivots = await db(this.pivotTable).whereIn(this.foreignPivotKey, parentIds).get();
|
|
189
|
+
const pivots = await db(this.pivotTable, this.related.connection).whereIn(this.foreignPivotKey, parentIds).get();
|
|
190
190
|
const relatedIds = unique(pivots.map((p) => p[this.relatedPivotKey]));
|
|
191
191
|
const rows = relatedIds.length
|
|
192
|
-
? await db(this.related.table).whereIn(this.relatedKey, relatedIds).get()
|
|
192
|
+
? await db(this.related.table, this.related.connection).whereIn(this.relatedKey, relatedIds).get()
|
|
193
193
|
: [];
|
|
194
194
|
const relatedById = new Map(rows.map((row) => [row[this.relatedKey], row]));
|
|
195
195
|
const grouped = new Map();
|
|
@@ -207,7 +207,7 @@ export class BelongsToMany extends Relation {
|
|
|
207
207
|
}
|
|
208
208
|
/** Attach a related row by linking it through the pivot table. */
|
|
209
209
|
async attach(id, extra = {}) {
|
|
210
|
-
await db(this.pivotTable).insert({
|
|
210
|
+
await db(this.pivotTable, this.related.connection).insert({
|
|
211
211
|
[this.foreignPivotKey]: this.parentValue(),
|
|
212
212
|
[this.relatedPivotKey]: id,
|
|
213
213
|
...extra,
|
|
@@ -215,7 +215,7 @@ export class BelongsToMany extends Relation {
|
|
|
215
215
|
}
|
|
216
216
|
/** Detach one related row (or all, when no id is given). */
|
|
217
217
|
async detach(id) {
|
|
218
|
-
let q = db(this.pivotTable).where(this.foreignPivotKey, this.parentValue());
|
|
218
|
+
let q = db(this.pivotTable, this.related.connection).where(this.foreignPivotKey, this.parentValue());
|
|
219
219
|
if (id !== undefined)
|
|
220
220
|
q = q.where(this.relatedPivotKey, id);
|
|
221
221
|
await q.delete();
|
package/dist/core/request.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ interface ResponseHelper {
|
|
|
33
33
|
text(body: string, status?: number): Response;
|
|
34
34
|
html(body: string, status?: number): Response;
|
|
35
35
|
redirect(location: string, status?: number): Response;
|
|
36
|
+
/** Redirect to the `Referer` header, or `fallback` (default "/") if absent. */
|
|
37
|
+
back(fallback?: string, status?: number): Response;
|
|
36
38
|
/** Send a value — objects become JSON, everything else becomes text. */
|
|
37
39
|
send(data: unknown, status?: number): Response;
|
|
38
40
|
/** Set the response status (chainable). */
|
|
@@ -47,6 +49,8 @@ interface ResponseHelper {
|
|
|
47
49
|
hasHeader(name: string): boolean;
|
|
48
50
|
/** Set the Content-Type (chainable). */
|
|
49
51
|
type(mime: string): ResponseHelper;
|
|
52
|
+
/** Mark the response as a downloadable attachment via Content-Disposition (chainable). */
|
|
53
|
+
attachment(filename?: string): ResponseHelper;
|
|
50
54
|
/** Append a value to a (possibly multi-value) header (chainable). */
|
|
51
55
|
append(name: string, value: string): ResponseHelper;
|
|
52
56
|
/** Remove a response header (chainable). */
|
|
@@ -74,6 +78,20 @@ export declare const request: {
|
|
|
74
78
|
readonly method: string;
|
|
75
79
|
readonly path: string;
|
|
76
80
|
readonly url: string;
|
|
81
|
+
/** The request protocol — "https" or "http" — honoring X-Forwarded-Proto. */
|
|
82
|
+
readonly protocol: string;
|
|
83
|
+
/** Whether the request came in over HTTPS. */
|
|
84
|
+
readonly secure: boolean;
|
|
85
|
+
/** The host with port, honoring X-Forwarded-Host (e.g. "example.com:443"). */
|
|
86
|
+
readonly host: string;
|
|
87
|
+
/** The host without port (e.g. "example.com"). */
|
|
88
|
+
readonly hostname: string;
|
|
89
|
+
/** Scheme + host — "https://example.com" — with no trailing slash. */
|
|
90
|
+
readonly origin: string;
|
|
91
|
+
/** The absolute request URL, rebuilt from the (proxy-aware) origin. */
|
|
92
|
+
readonly fullUrl: string;
|
|
93
|
+
/** The raw query string without the leading "?" (empty when none). */
|
|
94
|
+
readonly querystring: string;
|
|
77
95
|
/** The response status (useful after `await next()` in middleware). */
|
|
78
96
|
readonly status: number;
|
|
79
97
|
header(name: string): string | undefined;
|
|
@@ -128,6 +146,14 @@ export declare const request: {
|
|
|
128
146
|
language(languages: string[]): string | null;
|
|
129
147
|
/** Accepted languages, ordered by preference. */
|
|
130
148
|
languages(): string[];
|
|
149
|
+
/** The best of the offered content encodings per Accept-Encoding, or null. */
|
|
150
|
+
encoding(encodings: string[]): string | null;
|
|
151
|
+
/** Accepted content encodings, ordered by preference. */
|
|
152
|
+
encodings(): string[];
|
|
153
|
+
/** The best of the offered charsets per Accept-Charset, or null. */
|
|
154
|
+
charset(charsets: string[]): string | null;
|
|
155
|
+
/** Accepted charsets, ordered by preference. */
|
|
156
|
+
charsets(): string[];
|
|
131
157
|
/** A single input (from query or body), with an optional fallback (async). */
|
|
132
158
|
input<T = unknown>(key: string, fallback?: T): Promise<T>;
|
|
133
159
|
/** Only the named inputs (async). */
|
package/dist/core/request.js
CHANGED
|
@@ -67,6 +67,20 @@ function negotiate(headerName, offered) {
|
|
|
67
67
|
return a;
|
|
68
68
|
return null;
|
|
69
69
|
}
|
|
70
|
+
/* ------------------------------ url / host ----------------------------- */
|
|
71
|
+
/* Proxy-aware URL introspection: X-Forwarded-* wins over the raw URL, so an
|
|
72
|
+
* app behind a TLS-terminating proxy sees the client's protocol and host. */
|
|
73
|
+
/** The first value of a (possibly comma-listed) header. */
|
|
74
|
+
function firstForwarded(name) {
|
|
75
|
+
const v = ctx().req.header(name);
|
|
76
|
+
return v ? v.split(",")[0].trim() : undefined;
|
|
77
|
+
}
|
|
78
|
+
function requestProtocol() {
|
|
79
|
+
return firstForwarded("x-forwarded-proto") ?? new URL(ctx().req.url).protocol.replace(/:$/, "");
|
|
80
|
+
}
|
|
81
|
+
function requestHost() {
|
|
82
|
+
return firstForwarded("x-forwarded-host") ?? ctx().req.header("host") ?? new URL(ctx().req.url).host;
|
|
83
|
+
}
|
|
70
84
|
/* ------------------------------ responses ------------------------------ */
|
|
71
85
|
/* These work inside a handler AND standalone (e.g. as a static route value).
|
|
72
86
|
* Inside a request they build on the context (merging any headers); outside a
|
|
@@ -98,6 +112,9 @@ export function html(body, status) {
|
|
|
98
112
|
}
|
|
99
113
|
export function redirect(location, status) {
|
|
100
114
|
const c = maybeCtx();
|
|
115
|
+
// Koa-style `redirect("back")`: bounce to the Referer, or "/" if there isn't one.
|
|
116
|
+
if (location === "back")
|
|
117
|
+
location = c?.req.header("referer") ?? "/";
|
|
101
118
|
return c
|
|
102
119
|
? c.redirect(location, status)
|
|
103
120
|
: new Response(null, { status: status ?? 302, headers: { location } });
|
|
@@ -115,6 +132,9 @@ export const response = {
|
|
|
115
132
|
redirect(location, status) {
|
|
116
133
|
return redirect(location, status);
|
|
117
134
|
},
|
|
135
|
+
back(fallback = "/", status) {
|
|
136
|
+
return redirect(ctx().req.header("referer") ?? fallback, status);
|
|
137
|
+
},
|
|
118
138
|
send(data, status) {
|
|
119
139
|
return typeof data === "object" && data !== null
|
|
120
140
|
? json(data, status)
|
|
@@ -143,6 +163,18 @@ export const response = {
|
|
|
143
163
|
ctx().header("content-type", mime);
|
|
144
164
|
return response;
|
|
145
165
|
},
|
|
166
|
+
attachment(filename) {
|
|
167
|
+
if (filename === undefined) {
|
|
168
|
+
ctx().header("content-disposition", "attachment");
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// Quote the ASCII-safe name; add RFC 5987 filename* for anything else.
|
|
172
|
+
const ascii = filename.replace(/[^\x20-\x7e]/g, "?").replace(/["\\]/g, "_");
|
|
173
|
+
const encoded = encodeURIComponent(filename);
|
|
174
|
+
ctx().header("content-disposition", `attachment; filename="${ascii}"; filename*=UTF-8''${encoded}`);
|
|
175
|
+
}
|
|
176
|
+
return response;
|
|
177
|
+
},
|
|
146
178
|
append(name, value) {
|
|
147
179
|
ctx().header(name, value, { append: true });
|
|
148
180
|
return response;
|
|
@@ -189,6 +221,35 @@ export const request = {
|
|
|
189
221
|
get url() {
|
|
190
222
|
return ctx().req.url;
|
|
191
223
|
},
|
|
224
|
+
/** The request protocol — "https" or "http" — honoring X-Forwarded-Proto. */
|
|
225
|
+
get protocol() {
|
|
226
|
+
return requestProtocol();
|
|
227
|
+
},
|
|
228
|
+
/** Whether the request came in over HTTPS. */
|
|
229
|
+
get secure() {
|
|
230
|
+
return requestProtocol() === "https";
|
|
231
|
+
},
|
|
232
|
+
/** The host with port, honoring X-Forwarded-Host (e.g. "example.com:443"). */
|
|
233
|
+
get host() {
|
|
234
|
+
return requestHost();
|
|
235
|
+
},
|
|
236
|
+
/** The host without port (e.g. "example.com"). */
|
|
237
|
+
get hostname() {
|
|
238
|
+
return requestHost().split(":")[0];
|
|
239
|
+
},
|
|
240
|
+
/** Scheme + host — "https://example.com" — with no trailing slash. */
|
|
241
|
+
get origin() {
|
|
242
|
+
return `${requestProtocol()}://${requestHost()}`;
|
|
243
|
+
},
|
|
244
|
+
/** The absolute request URL, rebuilt from the (proxy-aware) origin. */
|
|
245
|
+
get fullUrl() {
|
|
246
|
+
const u = new URL(ctx().req.url);
|
|
247
|
+
return `${requestProtocol()}://${requestHost()}${u.pathname}${u.search}`;
|
|
248
|
+
},
|
|
249
|
+
/** The raw query string without the leading "?" (empty when none). */
|
|
250
|
+
get querystring() {
|
|
251
|
+
return new URL(ctx().req.url).search.replace(/^\?/, "");
|
|
252
|
+
},
|
|
192
253
|
/** The response status (useful after `await next()` in middleware). */
|
|
193
254
|
get status() {
|
|
194
255
|
return ctx().res.status;
|
|
@@ -332,6 +393,22 @@ export const request = {
|
|
|
332
393
|
languages() {
|
|
333
394
|
return parseAccept(ctx().req.header("accept-language"));
|
|
334
395
|
},
|
|
396
|
+
/** The best of the offered content encodings per Accept-Encoding, or null. */
|
|
397
|
+
encoding(encodings) {
|
|
398
|
+
return negotiate("accept-encoding", encodings);
|
|
399
|
+
},
|
|
400
|
+
/** Accepted content encodings, ordered by preference. */
|
|
401
|
+
encodings() {
|
|
402
|
+
return parseAccept(ctx().req.header("accept-encoding"));
|
|
403
|
+
},
|
|
404
|
+
/** The best of the offered charsets per Accept-Charset, or null. */
|
|
405
|
+
charset(charsets) {
|
|
406
|
+
return negotiate("accept-charset", charsets);
|
|
407
|
+
},
|
|
408
|
+
/** Accepted charsets, ordered by preference. */
|
|
409
|
+
charsets() {
|
|
410
|
+
return parseAccept(ctx().req.header("accept-charset"));
|
|
411
|
+
},
|
|
335
412
|
/** A single input (from query or body), with an optional fallback (async). */
|
|
336
413
|
async input(key, fallback) {
|
|
337
414
|
const all = await this.all();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security headers — the "shield" for server-rendered apps. One middleware that
|
|
3
|
+
* sets the defensive HTTP headers browsers act on: a Content-Security-Policy,
|
|
4
|
+
* HSTS, clickjacking and MIME-sniffing guards, and a referrer policy.
|
|
5
|
+
*
|
|
6
|
+
* this.use(securityHeaders()); // sensible defaults
|
|
7
|
+
*
|
|
8
|
+
* this.use(securityHeaders({
|
|
9
|
+
* csp: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "https://cdn.example.com"] },
|
|
10
|
+
* hsts: { maxAge: 15552000, includeSubDomains: true },
|
|
11
|
+
* frameGuard: "DENY",
|
|
12
|
+
* }));
|
|
13
|
+
*
|
|
14
|
+
* Each header can be turned off with `false`. CSP takes a ready-made string or a
|
|
15
|
+
* directives object whose camelCase keys become `kebab-case` (`defaultSrc` →
|
|
16
|
+
* `default-src`). Pair with [`csrf()`](./csrf.ts) for form protection.
|
|
17
|
+
*/
|
|
18
|
+
import type { MiddlewareHandler } from "hono";
|
|
19
|
+
export interface HstsOptions {
|
|
20
|
+
/** Max-age in seconds. Default 180 days. */
|
|
21
|
+
maxAge?: number;
|
|
22
|
+
/** Apply to subdomains too. Default true. */
|
|
23
|
+
includeSubDomains?: boolean;
|
|
24
|
+
/** Add `preload` (only if you've submitted to the HSTS preload list). Default false. */
|
|
25
|
+
preload?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface SecurityHeadersOptions {
|
|
28
|
+
/** Content-Security-Policy — a raw string, a directives object, or `false` to omit. */
|
|
29
|
+
csp?: string | Record<string, string[]> | false;
|
|
30
|
+
/** Strict-Transport-Security. `true` for defaults, an object to tune, `false` to omit. Default off unless set. */
|
|
31
|
+
hsts?: boolean | HstsOptions;
|
|
32
|
+
/** X-Frame-Options clickjacking guard. Default `"SAMEORIGIN"`; `false` to omit. */
|
|
33
|
+
frameGuard?: false | "DENY" | "SAMEORIGIN";
|
|
34
|
+
/** X-Content-Type-Options: nosniff. Default true. */
|
|
35
|
+
noSniff?: boolean;
|
|
36
|
+
/** Referrer-Policy. Default `"strict-origin-when-cross-origin"`; `false` to omit. */
|
|
37
|
+
referrerPolicy?: string | false;
|
|
38
|
+
}
|
|
39
|
+
export declare function securityHeaders(options?: SecurityHeadersOptions): MiddlewareHandler;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security headers — the "shield" for server-rendered apps. One middleware that
|
|
3
|
+
* sets the defensive HTTP headers browsers act on: a Content-Security-Policy,
|
|
4
|
+
* HSTS, clickjacking and MIME-sniffing guards, and a referrer policy.
|
|
5
|
+
*
|
|
6
|
+
* this.use(securityHeaders()); // sensible defaults
|
|
7
|
+
*
|
|
8
|
+
* this.use(securityHeaders({
|
|
9
|
+
* csp: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "https://cdn.example.com"] },
|
|
10
|
+
* hsts: { maxAge: 15552000, includeSubDomains: true },
|
|
11
|
+
* frameGuard: "DENY",
|
|
12
|
+
* }));
|
|
13
|
+
*
|
|
14
|
+
* Each header can be turned off with `false`. CSP takes a ready-made string or a
|
|
15
|
+
* directives object whose camelCase keys become `kebab-case` (`defaultSrc` →
|
|
16
|
+
* `default-src`). Pair with [`csrf()`](./csrf.ts) for form protection.
|
|
17
|
+
*/
|
|
18
|
+
/** camelCase directive names → the kebab-case CSP spelling. */
|
|
19
|
+
function buildCsp(directives) {
|
|
20
|
+
return Object.entries(directives)
|
|
21
|
+
.map(([key, values]) => {
|
|
22
|
+
const name = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
23
|
+
return values.length ? `${name} ${values.join(" ")}` : name;
|
|
24
|
+
})
|
|
25
|
+
.join("; ");
|
|
26
|
+
}
|
|
27
|
+
export function securityHeaders(options = {}) {
|
|
28
|
+
const frame = options.frameGuard === undefined ? "SAMEORIGIN" : options.frameGuard;
|
|
29
|
+
const referrer = options.referrerPolicy === undefined ? "strict-origin-when-cross-origin" : options.referrerPolicy;
|
|
30
|
+
const noSniff = options.noSniff !== false;
|
|
31
|
+
// Precompute the static header values once.
|
|
32
|
+
const csp = options.csp === false || options.csp === undefined
|
|
33
|
+
? null
|
|
34
|
+
: typeof options.csp === "string"
|
|
35
|
+
? options.csp
|
|
36
|
+
: buildCsp(options.csp);
|
|
37
|
+
let hstsValue = null;
|
|
38
|
+
if (options.hsts) {
|
|
39
|
+
const h = options.hsts === true ? {} : options.hsts;
|
|
40
|
+
const parts = [`max-age=${h.maxAge ?? 15552000}`];
|
|
41
|
+
if (h.includeSubDomains !== false)
|
|
42
|
+
parts.push("includeSubDomains");
|
|
43
|
+
if (h.preload)
|
|
44
|
+
parts.push("preload");
|
|
45
|
+
hstsValue = parts.join("; ");
|
|
46
|
+
}
|
|
47
|
+
return async (c, next) => {
|
|
48
|
+
await next();
|
|
49
|
+
if (csp)
|
|
50
|
+
c.header("Content-Security-Policy", csp);
|
|
51
|
+
if (hstsValue)
|
|
52
|
+
c.header("Strict-Transport-Security", hstsValue);
|
|
53
|
+
if (frame)
|
|
54
|
+
c.header("X-Frame-Options", frame);
|
|
55
|
+
if (noSniff)
|
|
56
|
+
c.header("X-Content-Type-Options", "nosniff");
|
|
57
|
+
if (referrer)
|
|
58
|
+
c.header("Referrer-Policy", referrer);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social authentication — OAuth 2.0 "sign in with GitHub/Google/…". Like Adonis
|
|
3
|
+
* Ally, this owns the OAuth dance only: it hands you a normalized `SocialUser`,
|
|
4
|
+
* and *you* find-or-create your own user and log them in (with a session,
|
|
5
|
+
* `jwt`, or an access `token`). It stores nothing.
|
|
6
|
+
*
|
|
7
|
+
* const github = social.github({ clientId, clientSecret, redirectUri });
|
|
8
|
+
*
|
|
9
|
+
* // 1. send the user off to the provider
|
|
10
|
+
* router.get("/auth/github", () => redirect(github.redirect({ state })));
|
|
11
|
+
*
|
|
12
|
+
* // 2. handle the callback
|
|
13
|
+
* router.get("/auth/github/callback", async () => {
|
|
14
|
+
* const gh = await github.user(request.query("code")); // { id, email, name, … }
|
|
15
|
+
* const user = await users.firstOrCreate({ github_id: gh.id }, { email: gh.email });
|
|
16
|
+
* auth().login(user.id);
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* Every driver is `fetch`-based — no SDK, no native deps — so it runs on Node and
|
|
20
|
+
* the edge alike. Presets cover GitHub, Google, and Discord; build your own with
|
|
21
|
+
* `oauthDriver()` for anything else OAuth2.
|
|
22
|
+
*/
|
|
23
|
+
/** An OAuth token set returned by the provider's token endpoint. */
|
|
24
|
+
export interface OAuthToken {
|
|
25
|
+
accessToken: string;
|
|
26
|
+
tokenType?: string;
|
|
27
|
+
refreshToken?: string;
|
|
28
|
+
/** Seconds until the access token expires, if the provider says. */
|
|
29
|
+
expiresIn?: number;
|
|
30
|
+
scope?: string;
|
|
31
|
+
/** The raw token response, for provider-specific fields. */
|
|
32
|
+
raw: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A provider's user, normalized to a common shape across every driver. `Token`
|
|
36
|
+
* is the OAuth2 `OAuthToken` by default, or an `OAuth1Token` for OAuth 1.0a
|
|
37
|
+
* providers.
|
|
38
|
+
*/
|
|
39
|
+
export interface SocialUser<Token = OAuthToken> {
|
|
40
|
+
/** The provider's stable id for this user (always a string). */
|
|
41
|
+
id: string;
|
|
42
|
+
email: string | null;
|
|
43
|
+
name: string | null;
|
|
44
|
+
/** Username / handle (e.g. GitHub login, Discord username). */
|
|
45
|
+
nickname: string | null;
|
|
46
|
+
avatarUrl: string | null;
|
|
47
|
+
/** The token used to fetch this profile — for calling the provider's API. */
|
|
48
|
+
token: Token;
|
|
49
|
+
/** The raw provider profile, for fields not in the normalized shape. */
|
|
50
|
+
raw: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
export interface OAuthConfig {
|
|
53
|
+
clientId: string;
|
|
54
|
+
clientSecret: string;
|
|
55
|
+
/** The callback URL registered with the provider. */
|
|
56
|
+
redirectUri: string;
|
|
57
|
+
/** Override the provider's default scopes. */
|
|
58
|
+
scopes?: string[];
|
|
59
|
+
}
|
|
60
|
+
export interface RedirectOptions {
|
|
61
|
+
/** A CSRF `state` value — generate with `oauthState()`, stash it, verify on callback. */
|
|
62
|
+
state?: string;
|
|
63
|
+
/** Scopes for this redirect (overrides config + provider defaults). */
|
|
64
|
+
scopes?: string[];
|
|
65
|
+
/** Extra query parameters to add to the authorize URL (e.g. `prompt`, `access_type`). */
|
|
66
|
+
params?: Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
/** The provider-specific bits an `OAuthDriver` needs. */
|
|
69
|
+
export interface ProviderSpec {
|
|
70
|
+
name: string;
|
|
71
|
+
authorizeUrl: string;
|
|
72
|
+
tokenUrl: string;
|
|
73
|
+
defaultScopes: string[];
|
|
74
|
+
/** How scopes are joined in the URL — space for most, comma for a few. */
|
|
75
|
+
scopeSeparator?: string;
|
|
76
|
+
/** Fetch and normalize the provider's user for an access token. */
|
|
77
|
+
fetchUser(token: OAuthToken): Promise<SocialUser>;
|
|
78
|
+
}
|
|
79
|
+
/** Thrown when the token exchange or profile fetch fails. */
|
|
80
|
+
export declare class OAuthError extends Error {
|
|
81
|
+
readonly provider: string;
|
|
82
|
+
constructor(message: string, provider: string);
|
|
83
|
+
}
|
|
84
|
+
/** A random, URL-safe `state` for CSRF protection — stash it, then verify on callback. */
|
|
85
|
+
export declare function oauthState(bytes?: number): string;
|
|
86
|
+
/** A generic OAuth 2.0 authorization-code driver. */
|
|
87
|
+
export declare class OAuthDriver {
|
|
88
|
+
private spec;
|
|
89
|
+
private config;
|
|
90
|
+
constructor(spec: ProviderSpec, config: OAuthConfig);
|
|
91
|
+
/** Build the provider's authorize URL to redirect the user to. */
|
|
92
|
+
redirect(options?: RedirectOptions): string;
|
|
93
|
+
/** Exchange an authorization `code` (from the callback) for an access token. */
|
|
94
|
+
exchangeCode(code: string): Promise<OAuthToken>;
|
|
95
|
+
/** Fetch the normalized user for an already-obtained access token. */
|
|
96
|
+
userFromToken(token: OAuthToken): Promise<SocialUser>;
|
|
97
|
+
/** The full callback step: exchange the `code`, then fetch the user. */
|
|
98
|
+
user(code: string): Promise<SocialUser>;
|
|
99
|
+
}
|
|
100
|
+
/** Build a driver for any OAuth2 provider from a spec + config. */
|
|
101
|
+
export declare function oauthDriver(spec: ProviderSpec, config: OAuthConfig): OAuthDriver;
|
|
102
|
+
/** GitHub OAuth (`user:email` gives access to a verified primary email). */
|
|
103
|
+
export declare function github(config: OAuthConfig): OAuthDriver;
|
|
104
|
+
/** Google OAuth / OpenID Connect. */
|
|
105
|
+
export declare function google(config: OAuthConfig): OAuthDriver;
|
|
106
|
+
/** Discord OAuth. */
|
|
107
|
+
export declare function discord(config: OAuthConfig): OAuthDriver;
|
|
108
|
+
export interface OAuth1Config {
|
|
109
|
+
/** Consumer (API) key. */
|
|
110
|
+
clientId: string;
|
|
111
|
+
/** Consumer (API) secret. */
|
|
112
|
+
clientSecret: string;
|
|
113
|
+
/** The `oauth_callback` URL registered with the provider. */
|
|
114
|
+
redirectUri: string;
|
|
115
|
+
}
|
|
116
|
+
/** An OAuth 1.0a token pair — both the request token and the final access token. */
|
|
117
|
+
export interface OAuth1Token {
|
|
118
|
+
token: string;
|
|
119
|
+
tokenSecret: string;
|
|
120
|
+
raw: Record<string, string>;
|
|
121
|
+
}
|
|
122
|
+
export interface OAuth1ProviderSpec {
|
|
123
|
+
name: string;
|
|
124
|
+
requestTokenUrl: string;
|
|
125
|
+
authorizeUrl: string;
|
|
126
|
+
accessTokenUrl: string;
|
|
127
|
+
fetchUser(token: OAuth1Token, driver: OAuth1Driver): Promise<SocialUser<OAuth1Token>>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Compute an OAuth 1.0a HMAC-SHA1 signature (RFC 5849). `params` holds every
|
|
131
|
+
* signed parameter with *raw* (unencoded) values — the oauth_* fields plus any
|
|
132
|
+
* query/body params, minus `oauth_signature`. Exposed for signing custom API
|
|
133
|
+
* requests beyond the built-in flow.
|
|
134
|
+
*/
|
|
135
|
+
export declare function oauth1Signature(input: {
|
|
136
|
+
method: string;
|
|
137
|
+
url: string;
|
|
138
|
+
params: Record<string, string>;
|
|
139
|
+
consumerSecret: string;
|
|
140
|
+
tokenSecret?: string;
|
|
141
|
+
}): Promise<string>;
|
|
142
|
+
/** A generic OAuth 1.0a driver. */
|
|
143
|
+
export declare class OAuth1Driver {
|
|
144
|
+
private spec;
|
|
145
|
+
private config;
|
|
146
|
+
constructor(spec: OAuth1ProviderSpec, config: OAuth1Config);
|
|
147
|
+
/** Sign a request and build its `Authorization: OAuth …` header. */
|
|
148
|
+
private authHeader;
|
|
149
|
+
/** Step 1 — obtain a temporary request token. Stash its `tokenSecret` for the callback. */
|
|
150
|
+
requestToken(): Promise<OAuth1Token>;
|
|
151
|
+
/** Step 2 — the URL to send the user to, carrying the request token. */
|
|
152
|
+
redirect(requestToken: string | OAuth1Token): string;
|
|
153
|
+
/** Step 3 — swap the callback's `oauth_token` + `oauth_verifier` for an access token. */
|
|
154
|
+
accessToken(oauthToken: string, verifier: string, requestTokenSecret: string): Promise<OAuth1Token>;
|
|
155
|
+
/** The full callback step: exchange for an access token, then fetch the user. */
|
|
156
|
+
user(oauthToken: string, verifier: string, requestTokenSecret: string): Promise<SocialUser<OAuth1Token>>;
|
|
157
|
+
/** A signed GET against the provider's API on the user's behalf (for `fetchUser`). */
|
|
158
|
+
get(url: string, token: OAuth1Token): Promise<Record<string, unknown>>;
|
|
159
|
+
}
|
|
160
|
+
/** Build a driver for any OAuth 1.0a provider from a spec + config. */
|
|
161
|
+
export declare function oauth1Driver(spec: OAuth1ProviderSpec, config: OAuth1Config): OAuth1Driver;
|
|
162
|
+
/** Twitter / X (OAuth 1.0a). Enable "Request email" in your app settings for `email`. */
|
|
163
|
+
export declare function twitter(config: OAuth1Config): OAuth1Driver;
|
|
164
|
+
/** All social providers under one namespace: `social.github({...})`, `social.twitter({...})`. */
|
|
165
|
+
export declare const social: {
|
|
166
|
+
github: typeof github;
|
|
167
|
+
google: typeof google;
|
|
168
|
+
discord: typeof discord;
|
|
169
|
+
driver: typeof oauthDriver;
|
|
170
|
+
state: typeof oauthState;
|
|
171
|
+
twitter: typeof twitter;
|
|
172
|
+
driver1: typeof oauth1Driver;
|
|
173
|
+
};
|