@shaferllc/keel 0.59.0 → 0.68.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/AGENTS.md +167 -0
- package/README.md +30 -1
- package/bin/keel-mcp.mjs +9 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +14 -2
- 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/cache.d.ts +82 -5
- package/dist/core/cache.js +181 -23
- package/dist/core/cli/stubs.d.ts +12 -0
- package/dist/core/cli/stubs.js +120 -0
- 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/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +15 -3
- package/dist/core/helpers.js +23 -3
- package/dist/core/index.d.ts +33 -18
- package/dist/core/index.js +16 -8
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +82 -4
- package/dist/core/logger.js +141 -23
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- 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/queue.d.ts +134 -9
- package/dist/core/queue.js +304 -14
- 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/storage.d.ts +159 -6
- package/dist/core/storage.js +287 -7
- 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/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/docs/ai-manifest.json +2472 -0
- package/docs/ai.md +128 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +311 -0
- package/docs/console.md +356 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +530 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +81 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +118 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +92 -0
- package/docs/examples/mail.ts +160 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +66 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +290 -0
- package/docs/mail.md +678 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +450 -0
- package/docs/templates.md +315 -0
- package/docs/testing.md +125 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/llms-full.txt +17694 -0
- package/llms.txt +116 -0
- package/package.json +38 -7
|
@@ -0,0 +1,337 @@
|
|
|
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
|
+
/** Thrown when the token exchange or profile fetch fails. */
|
|
24
|
+
export class OAuthError extends Error {
|
|
25
|
+
provider;
|
|
26
|
+
constructor(message, provider) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.provider = provider;
|
|
29
|
+
this.name = "OAuthError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** A random, URL-safe `state` for CSRF protection — stash it, then verify on callback. */
|
|
33
|
+
export function oauthState(bytes = 16) {
|
|
34
|
+
const raw = crypto.getRandomValues(new Uint8Array(bytes));
|
|
35
|
+
let s = "";
|
|
36
|
+
for (const b of raw)
|
|
37
|
+
s += String.fromCharCode(b);
|
|
38
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
39
|
+
}
|
|
40
|
+
/** A generic OAuth 2.0 authorization-code driver. */
|
|
41
|
+
export class OAuthDriver {
|
|
42
|
+
spec;
|
|
43
|
+
config;
|
|
44
|
+
constructor(spec, config) {
|
|
45
|
+
this.spec = spec;
|
|
46
|
+
this.config = config;
|
|
47
|
+
}
|
|
48
|
+
/** Build the provider's authorize URL to redirect the user to. */
|
|
49
|
+
redirect(options = {}) {
|
|
50
|
+
const scopes = options.scopes ?? this.config.scopes ?? this.spec.defaultScopes;
|
|
51
|
+
const url = new URL(this.spec.authorizeUrl);
|
|
52
|
+
url.searchParams.set("client_id", this.config.clientId);
|
|
53
|
+
url.searchParams.set("redirect_uri", this.config.redirectUri);
|
|
54
|
+
url.searchParams.set("response_type", "code");
|
|
55
|
+
if (scopes.length)
|
|
56
|
+
url.searchParams.set("scope", scopes.join(this.spec.scopeSeparator ?? " "));
|
|
57
|
+
if (options.state)
|
|
58
|
+
url.searchParams.set("state", options.state);
|
|
59
|
+
for (const [key, value] of Object.entries(options.params ?? {}))
|
|
60
|
+
url.searchParams.set(key, value);
|
|
61
|
+
return url.toString();
|
|
62
|
+
}
|
|
63
|
+
/** Exchange an authorization `code` (from the callback) for an access token. */
|
|
64
|
+
async exchangeCode(code) {
|
|
65
|
+
const res = await fetch(this.spec.tokenUrl, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
68
|
+
body: new URLSearchParams({
|
|
69
|
+
client_id: this.config.clientId,
|
|
70
|
+
client_secret: this.config.clientSecret,
|
|
71
|
+
code,
|
|
72
|
+
redirect_uri: this.config.redirectUri,
|
|
73
|
+
grant_type: "authorization_code",
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
const data = (await res.json().catch(() => ({})));
|
|
77
|
+
if (!res.ok || data.error) {
|
|
78
|
+
throw new OAuthError(`Token exchange failed: ${data.error_description ?? data.error ?? res.status}`, this.spec.name);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
accessToken: String(data.access_token),
|
|
82
|
+
tokenType: data.token_type,
|
|
83
|
+
refreshToken: data.refresh_token,
|
|
84
|
+
expiresIn: data.expires_in,
|
|
85
|
+
scope: data.scope,
|
|
86
|
+
raw: data,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Fetch the normalized user for an already-obtained access token. */
|
|
90
|
+
userFromToken(token) {
|
|
91
|
+
return this.spec.fetchUser(token);
|
|
92
|
+
}
|
|
93
|
+
/** The full callback step: exchange the `code`, then fetch the user. */
|
|
94
|
+
async user(code) {
|
|
95
|
+
return this.userFromToken(await this.exchangeCode(code));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Build a driver for any OAuth2 provider from a spec + config. */
|
|
99
|
+
export function oauthDriver(spec, config) {
|
|
100
|
+
return new OAuthDriver(spec, config);
|
|
101
|
+
}
|
|
102
|
+
/* -------------------------------- helpers ------------------------------- */
|
|
103
|
+
async function getJson(url, token, headers = {}) {
|
|
104
|
+
const res = await fetch(url, {
|
|
105
|
+
headers: { authorization: `Bearer ${token.accessToken}`, accept: "application/json", ...headers },
|
|
106
|
+
});
|
|
107
|
+
return (await res.json().catch(() => ({})));
|
|
108
|
+
}
|
|
109
|
+
/* ------------------------------- providers ------------------------------ */
|
|
110
|
+
/** GitHub OAuth (`user:email` gives access to a verified primary email). */
|
|
111
|
+
export function github(config) {
|
|
112
|
+
return new OAuthDriver({
|
|
113
|
+
name: "github",
|
|
114
|
+
authorizeUrl: "https://github.com/login/oauth/authorize",
|
|
115
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
116
|
+
defaultScopes: ["read:user", "user:email"],
|
|
117
|
+
async fetchUser(token) {
|
|
118
|
+
const headers = { "user-agent": "keel", accept: "application/vnd.github+json" };
|
|
119
|
+
const data = await getJson("https://api.github.com/user", token, headers);
|
|
120
|
+
let email = data.email ?? null;
|
|
121
|
+
if (!email) {
|
|
122
|
+
// The public profile hides email unless set — pull the verified primary.
|
|
123
|
+
const emails = (await getJson("https://api.github.com/user/emails", token, headers));
|
|
124
|
+
if (Array.isArray(emails)) {
|
|
125
|
+
email = emails.find((e) => e.primary && e.verified)?.email ?? emails[0]?.email ?? null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
id: String(data.id),
|
|
130
|
+
email,
|
|
131
|
+
name: data.name ?? null,
|
|
132
|
+
nickname: data.login ?? null,
|
|
133
|
+
avatarUrl: data.avatar_url ?? null,
|
|
134
|
+
token,
|
|
135
|
+
raw: data,
|
|
136
|
+
};
|
|
137
|
+
},
|
|
138
|
+
}, config);
|
|
139
|
+
}
|
|
140
|
+
/** Google OAuth / OpenID Connect. */
|
|
141
|
+
export function google(config) {
|
|
142
|
+
return new OAuthDriver({
|
|
143
|
+
name: "google",
|
|
144
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
145
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
146
|
+
defaultScopes: ["openid", "email", "profile"],
|
|
147
|
+
async fetchUser(token) {
|
|
148
|
+
const data = await getJson("https://openidconnect.googleapis.com/v1/userinfo", token);
|
|
149
|
+
return {
|
|
150
|
+
id: String(data.sub),
|
|
151
|
+
email: data.email ?? null,
|
|
152
|
+
name: data.name ?? null,
|
|
153
|
+
nickname: data.given_name ?? data.email ?? null,
|
|
154
|
+
avatarUrl: data.picture ?? null,
|
|
155
|
+
token,
|
|
156
|
+
raw: data,
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
}, config);
|
|
160
|
+
}
|
|
161
|
+
/** Discord OAuth. */
|
|
162
|
+
export function discord(config) {
|
|
163
|
+
return new OAuthDriver({
|
|
164
|
+
name: "discord",
|
|
165
|
+
authorizeUrl: "https://discord.com/oauth2/authorize",
|
|
166
|
+
tokenUrl: "https://discord.com/api/oauth2/token",
|
|
167
|
+
defaultScopes: ["identify", "email"],
|
|
168
|
+
async fetchUser(token) {
|
|
169
|
+
const data = await getJson("https://discord.com/api/users/@me", token);
|
|
170
|
+
const id = String(data.id);
|
|
171
|
+
const avatar = data.avatar;
|
|
172
|
+
return {
|
|
173
|
+
id,
|
|
174
|
+
email: data.email ?? null,
|
|
175
|
+
name: data.global_name ?? data.username ?? null,
|
|
176
|
+
nickname: data.username ?? null,
|
|
177
|
+
avatarUrl: avatar ? `https://cdn.discordapp.com/avatars/${id}/${avatar}.png` : null,
|
|
178
|
+
token,
|
|
179
|
+
raw: data,
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
}, config);
|
|
183
|
+
}
|
|
184
|
+
/** Percent-encode per RFC 3986 (OAuth's stricter rules — encodes `!*'()` too). */
|
|
185
|
+
function pctEncode(value) {
|
|
186
|
+
return encodeURIComponent(value).replace(/[!*'()]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
|
|
187
|
+
}
|
|
188
|
+
function bytesToB64(bytes) {
|
|
189
|
+
let s = "";
|
|
190
|
+
for (const b of bytes)
|
|
191
|
+
s += String.fromCharCode(b);
|
|
192
|
+
return btoa(s);
|
|
193
|
+
}
|
|
194
|
+
async function hmacSha1(base, key) {
|
|
195
|
+
const cryptoKey = await crypto.subtle.importKey("raw", new TextEncoder().encode(key), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);
|
|
196
|
+
const sig = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(base));
|
|
197
|
+
return bytesToB64(new Uint8Array(sig));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Compute an OAuth 1.0a HMAC-SHA1 signature (RFC 5849). `params` holds every
|
|
201
|
+
* signed parameter with *raw* (unencoded) values — the oauth_* fields plus any
|
|
202
|
+
* query/body params, minus `oauth_signature`. Exposed for signing custom API
|
|
203
|
+
* requests beyond the built-in flow.
|
|
204
|
+
*/
|
|
205
|
+
export async function oauth1Signature(input) {
|
|
206
|
+
const paramString = Object.keys(input.params)
|
|
207
|
+
.sort()
|
|
208
|
+
.map((k) => `${pctEncode(k)}=${pctEncode(input.params[k])}`)
|
|
209
|
+
.join("&");
|
|
210
|
+
const baseUrl = input.url.split(/[?#]/)[0]; // the base string excludes query/fragment
|
|
211
|
+
const base = [input.method.toUpperCase(), pctEncode(baseUrl), pctEncode(paramString)].join("&");
|
|
212
|
+
const key = `${pctEncode(input.consumerSecret)}&${pctEncode(input.tokenSecret ?? "")}`;
|
|
213
|
+
return hmacSha1(base, key);
|
|
214
|
+
}
|
|
215
|
+
function oauthNonce() {
|
|
216
|
+
return bytesToB64(crypto.getRandomValues(new Uint8Array(16))).replace(/[^A-Za-z0-9]/g, "");
|
|
217
|
+
}
|
|
218
|
+
function parseForm(body) {
|
|
219
|
+
const out = {};
|
|
220
|
+
for (const pair of body.split("&")) {
|
|
221
|
+
const i = pair.indexOf("=");
|
|
222
|
+
if (i === -1)
|
|
223
|
+
continue;
|
|
224
|
+
out[decodeURIComponent(pair.slice(0, i))] = decodeURIComponent(pair.slice(i + 1));
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
/** A generic OAuth 1.0a driver. */
|
|
229
|
+
export class OAuth1Driver {
|
|
230
|
+
spec;
|
|
231
|
+
config;
|
|
232
|
+
constructor(spec, config) {
|
|
233
|
+
this.spec = spec;
|
|
234
|
+
this.config = config;
|
|
235
|
+
}
|
|
236
|
+
/** Sign a request and build its `Authorization: OAuth …` header. */
|
|
237
|
+
async authHeader(method, url, extraOauth, tokenSecret, queryParams = {}) {
|
|
238
|
+
const oauth = {
|
|
239
|
+
oauth_consumer_key: this.config.clientId,
|
|
240
|
+
oauth_nonce: oauthNonce(),
|
|
241
|
+
oauth_signature_method: "HMAC-SHA1",
|
|
242
|
+
oauth_timestamp: String(Math.floor(Date.now() / 1000)),
|
|
243
|
+
oauth_version: "1.0",
|
|
244
|
+
...extraOauth,
|
|
245
|
+
};
|
|
246
|
+
const signature = await oauth1Signature({
|
|
247
|
+
method,
|
|
248
|
+
url,
|
|
249
|
+
params: { ...oauth, ...queryParams },
|
|
250
|
+
consumerSecret: this.config.clientSecret,
|
|
251
|
+
tokenSecret,
|
|
252
|
+
});
|
|
253
|
+
const header = { ...oauth, oauth_signature: signature };
|
|
254
|
+
return ("OAuth " +
|
|
255
|
+
Object.keys(header)
|
|
256
|
+
.sort()
|
|
257
|
+
.map((k) => `${pctEncode(k)}="${pctEncode(header[k])}"`)
|
|
258
|
+
.join(", "));
|
|
259
|
+
}
|
|
260
|
+
/** Step 1 — obtain a temporary request token. Stash its `tokenSecret` for the callback. */
|
|
261
|
+
async requestToken() {
|
|
262
|
+
const header = await this.authHeader("POST", this.spec.requestTokenUrl, { oauth_callback: this.config.redirectUri }, "");
|
|
263
|
+
const res = await fetch(this.spec.requestTokenUrl, { method: "POST", headers: { authorization: header } });
|
|
264
|
+
const body = await res.text();
|
|
265
|
+
if (!res.ok)
|
|
266
|
+
throw new OAuthError(`Request token failed: ${res.status} ${body}`, this.spec.name);
|
|
267
|
+
const parsed = parseForm(body);
|
|
268
|
+
return { token: parsed.oauth_token ?? "", tokenSecret: parsed.oauth_token_secret ?? "", raw: parsed };
|
|
269
|
+
}
|
|
270
|
+
/** Step 2 — the URL to send the user to, carrying the request token. */
|
|
271
|
+
redirect(requestToken) {
|
|
272
|
+
const t = typeof requestToken === "string" ? requestToken : requestToken.token;
|
|
273
|
+
const url = new URL(this.spec.authorizeUrl);
|
|
274
|
+
url.searchParams.set("oauth_token", t);
|
|
275
|
+
return url.toString();
|
|
276
|
+
}
|
|
277
|
+
/** Step 3 — swap the callback's `oauth_token` + `oauth_verifier` for an access token. */
|
|
278
|
+
async accessToken(oauthToken, verifier, requestTokenSecret) {
|
|
279
|
+
const header = await this.authHeader("POST", this.spec.accessTokenUrl, { oauth_token: oauthToken, oauth_verifier: verifier }, requestTokenSecret);
|
|
280
|
+
const res = await fetch(this.spec.accessTokenUrl, { method: "POST", headers: { authorization: header } });
|
|
281
|
+
const body = await res.text();
|
|
282
|
+
if (!res.ok)
|
|
283
|
+
throw new OAuthError(`Access token failed: ${res.status} ${body}`, this.spec.name);
|
|
284
|
+
const parsed = parseForm(body);
|
|
285
|
+
return { token: parsed.oauth_token ?? "", tokenSecret: parsed.oauth_token_secret ?? "", raw: parsed };
|
|
286
|
+
}
|
|
287
|
+
/** The full callback step: exchange for an access token, then fetch the user. */
|
|
288
|
+
async user(oauthToken, verifier, requestTokenSecret) {
|
|
289
|
+
const token = await this.accessToken(oauthToken, verifier, requestTokenSecret);
|
|
290
|
+
return this.spec.fetchUser(token, this);
|
|
291
|
+
}
|
|
292
|
+
/** A signed GET against the provider's API on the user's behalf (for `fetchUser`). */
|
|
293
|
+
async get(url, token) {
|
|
294
|
+
const query = {};
|
|
295
|
+
new URL(url).searchParams.forEach((v, k) => (query[k] = v));
|
|
296
|
+
const header = await this.authHeader("GET", url, { oauth_token: token.token }, token.tokenSecret, query);
|
|
297
|
+
const res = await fetch(url, { headers: { authorization: header } });
|
|
298
|
+
return (await res.json().catch(() => ({})));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/** Build a driver for any OAuth 1.0a provider from a spec + config. */
|
|
302
|
+
export function oauth1Driver(spec, config) {
|
|
303
|
+
return new OAuth1Driver(spec, config);
|
|
304
|
+
}
|
|
305
|
+
/** Twitter / X (OAuth 1.0a). Enable "Request email" in your app settings for `email`. */
|
|
306
|
+
export function twitter(config) {
|
|
307
|
+
return new OAuth1Driver({
|
|
308
|
+
name: "twitter",
|
|
309
|
+
requestTokenUrl: "https://api.twitter.com/oauth/request_token",
|
|
310
|
+
authorizeUrl: "https://api.twitter.com/oauth/authenticate",
|
|
311
|
+
accessTokenUrl: "https://api.twitter.com/oauth/access_token",
|
|
312
|
+
async fetchUser(token, driver) {
|
|
313
|
+
const data = await driver.get("https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true", token);
|
|
314
|
+
return {
|
|
315
|
+
id: String(data.id_str ?? data.id),
|
|
316
|
+
email: data.email ?? null,
|
|
317
|
+
name: data.name ?? null,
|
|
318
|
+
nickname: data.screen_name ?? null,
|
|
319
|
+
avatarUrl: data.profile_image_url_https ?? null,
|
|
320
|
+
token,
|
|
321
|
+
raw: data,
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
}, config);
|
|
325
|
+
}
|
|
326
|
+
/** All social providers under one namespace: `social.github({...})`, `social.twitter({...})`. */
|
|
327
|
+
export const social = {
|
|
328
|
+
// OAuth 2.0
|
|
329
|
+
github,
|
|
330
|
+
google,
|
|
331
|
+
discord,
|
|
332
|
+
driver: oauthDriver,
|
|
333
|
+
state: oauthState,
|
|
334
|
+
// OAuth 1.0a
|
|
335
|
+
twitter,
|
|
336
|
+
driver1: oauth1Driver,
|
|
337
|
+
};
|
package/dist/core/storage.d.ts
CHANGED
|
@@ -9,37 +9,119 @@
|
|
|
9
9
|
* const bytes = await storage().get("avatars/1.png");
|
|
10
10
|
* const url = storage().url("avatars/1.png");
|
|
11
11
|
*
|
|
12
|
+
* Private files get a temporary URL instead of a public one:
|
|
13
|
+
*
|
|
14
|
+
* await storage().signedUrl("invoices/42.pdf", { expiresIn: 300 });
|
|
15
|
+
*
|
|
16
|
+
* and browsers can upload straight to the bucket — the bytes never transit the
|
|
17
|
+
* app — with a signed upload URL:
|
|
18
|
+
*
|
|
19
|
+
* await storage().signedUploadUrl("uploads/raw.mov", { contentType: "video/quicktime" });
|
|
20
|
+
*
|
|
12
21
|
* Register several disks by name and select one with `storage("s3")`.
|
|
13
22
|
*/
|
|
14
|
-
|
|
23
|
+
import type { MiddlewareHandler } from "hono";
|
|
24
|
+
/** Whether an object is world-readable or needs a signed URL. */
|
|
25
|
+
export type FileVisibility = "public" | "private";
|
|
26
|
+
/** Per-write options. Disks map these onto their backend's own metadata. */
|
|
27
|
+
export interface WriteOptions {
|
|
28
|
+
/**
|
|
29
|
+
* The object's MIME type. Inferred from the path's extension when omitted, so
|
|
30
|
+
* a `.png` is stored as `image/png` rather than `application/octet-stream`.
|
|
31
|
+
*/
|
|
32
|
+
contentType?: string;
|
|
33
|
+
/** A `Cache-Control` value to store alongside the object. */
|
|
34
|
+
cacheControl?: string;
|
|
35
|
+
/** `"public"` or `"private"`. Disks that can't express this ignore it. */
|
|
36
|
+
visibility?: FileVisibility;
|
|
37
|
+
/** Arbitrary user metadata stored with the object. */
|
|
38
|
+
metadata?: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
/** What a disk knows about a stored object. */
|
|
41
|
+
export interface FileMetadata {
|
|
42
|
+
size: number;
|
|
43
|
+
contentType?: string;
|
|
44
|
+
cacheControl?: string;
|
|
45
|
+
visibility?: FileVisibility;
|
|
46
|
+
lastModified?: Date;
|
|
47
|
+
metadata?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
export interface SignedFileOptions {
|
|
50
|
+
/** Seconds the URL stays valid. Default: 3600 (one hour). */
|
|
51
|
+
expiresIn?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface SignedUploadOptions extends SignedFileOptions {
|
|
54
|
+
/** The `Content-Type` the uploader must send. */
|
|
55
|
+
contentType?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The bridge to a storage backend — implement it once per backend.
|
|
59
|
+
*
|
|
60
|
+
* The first six methods are required. The rest are optional capabilities: a disk
|
|
61
|
+
* that can do better than the generic implementation (native `HEAD`, a
|
|
62
|
+
* server-side copy, backend presigning) implements them, and `Storage` uses them
|
|
63
|
+
* when present.
|
|
64
|
+
*/
|
|
15
65
|
export interface Disk {
|
|
16
|
-
put(path: string, bytes: Uint8Array): Promise<void>;
|
|
66
|
+
put(path: string, bytes: Uint8Array, options?: WriteOptions): Promise<void>;
|
|
17
67
|
get(path: string): Promise<Uint8Array | null>;
|
|
18
68
|
exists(path: string): Promise<boolean>;
|
|
19
69
|
delete(path: string): Promise<void>;
|
|
20
70
|
/** Paths currently stored, optionally filtered to those under `prefix`. */
|
|
21
71
|
list(prefix?: string): Promise<string[]>;
|
|
22
|
-
/** A URL for the stored object
|
|
72
|
+
/** A public URL for the stored object. */
|
|
23
73
|
url(path: string): string;
|
|
74
|
+
/** Size, content type, and friends. `Storage.metadata()` falls back to a read. */
|
|
75
|
+
metadata?(path: string): Promise<FileMetadata | null>;
|
|
76
|
+
/** A server-side copy. `Storage.copy()` falls back to read-then-write. */
|
|
77
|
+
copy?(from: string, to: string): Promise<void>;
|
|
78
|
+
/** A server-side move. `Storage.move()` falls back to copy-then-delete. */
|
|
79
|
+
move?(from: string, to: string): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* A backend-presigned read URL (S3/R2 SigV4, GCS, …). When a disk doesn't
|
|
82
|
+
* implement this, `Storage.signedUrl()` signs the disk's own `url()` with
|
|
83
|
+
* `config('app.key')` — serve it with `serveStorage({ signed: true })`.
|
|
84
|
+
*/
|
|
85
|
+
signedUrl?(path: string, options?: SignedFileOptions): Promise<string>;
|
|
86
|
+
/**
|
|
87
|
+
* A backend-presigned upload URL, so a browser can `PUT` straight to the
|
|
88
|
+
* bucket. There is no generic fallback — only the backend can accept the write.
|
|
89
|
+
*/
|
|
90
|
+
signedUploadUrl?(path: string, options?: SignedUploadOptions): Promise<string>;
|
|
24
91
|
}
|
|
25
92
|
export type Contents = string | Uint8Array | ArrayBuffer;
|
|
93
|
+
/** The MIME type for a path's extension, or `application/octet-stream`. */
|
|
94
|
+
export declare function contentTypeFor(path: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* Sign a URL with `config('app.key')`, adding `expires` and `signature` query
|
|
97
|
+
* params. Verify it with `verifyStorageUrl()`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function signStorageUrl(url: string, expiresIn?: number): Promise<string>;
|
|
100
|
+
/** Whether a URL carries a valid, unexpired signature from `signStorageUrl()`. */
|
|
101
|
+
export declare function verifyStorageUrl(url: string): Promise<boolean>;
|
|
26
102
|
/** An in-memory `Disk` — the default; ideal for tests. Not shared across processes. */
|
|
27
103
|
export declare class MemoryDisk implements Disk {
|
|
28
104
|
private baseUrl;
|
|
29
105
|
private files;
|
|
30
106
|
constructor(baseUrl?: string);
|
|
31
|
-
put(path: string, bytes: Uint8Array): Promise<void>;
|
|
107
|
+
put(path: string, bytes: Uint8Array, options?: WriteOptions): Promise<void>;
|
|
32
108
|
get(path: string): Promise<Uint8Array | null>;
|
|
33
109
|
exists(path: string): Promise<boolean>;
|
|
34
110
|
delete(path: string): Promise<void>;
|
|
35
111
|
list(prefix?: string): Promise<string[]>;
|
|
36
112
|
url(path: string): string;
|
|
113
|
+
metadata(path: string): Promise<FileMetadata | null>;
|
|
114
|
+
copy(from: string, to: string): Promise<void>;
|
|
115
|
+
move(from: string, to: string): Promise<void>;
|
|
37
116
|
}
|
|
38
117
|
export declare class Storage {
|
|
39
118
|
private disk;
|
|
40
119
|
constructor(disk: Disk);
|
|
41
|
-
/**
|
|
42
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Write a file (string, bytes, or ArrayBuffer — strings are UTF-8 encoded).
|
|
122
|
+
* The content type is inferred from the extension unless you pass one.
|
|
123
|
+
*/
|
|
124
|
+
put(path: string, contents: Contents, options?: WriteOptions): Promise<void>;
|
|
43
125
|
/** Read a file's raw bytes, or null if it doesn't exist. */
|
|
44
126
|
get(path: string): Promise<Uint8Array | null>;
|
|
45
127
|
/** Read a file as UTF-8 text, or null if it doesn't exist. */
|
|
@@ -47,11 +129,82 @@ export declare class Storage {
|
|
|
47
129
|
exists(path: string): Promise<boolean>;
|
|
48
130
|
delete(path: string): Promise<void>;
|
|
49
131
|
list(prefix?: string): Promise<string[]>;
|
|
132
|
+
/** Size, content type, and friends — or null if the file doesn't exist. */
|
|
133
|
+
metadata(path: string): Promise<FileMetadata | null>;
|
|
134
|
+
/** A file's size in bytes, or null if it doesn't exist. */
|
|
135
|
+
size(path: string): Promise<number | null>;
|
|
136
|
+
/** Copy a file. Server-side when the disk supports it, else read-then-write. */
|
|
137
|
+
copy(from: string, to: string): Promise<void>;
|
|
138
|
+
/** Move a file. Server-side when the disk supports it, else copy-then-delete. */
|
|
139
|
+
move(from: string, to: string): Promise<void>;
|
|
140
|
+
/** A public URL for the object. Use `signedUrl()` for private ones. */
|
|
50
141
|
url(path: string): string;
|
|
142
|
+
/**
|
|
143
|
+
* A temporary URL for a private object. Uses the backend's own presigning when
|
|
144
|
+
* the disk implements `signedUrl` (S3, R2, GCS); otherwise signs the disk's
|
|
145
|
+
* `url()` with `config('app.key')` — serve those with `serveStorage({ signed: true })`.
|
|
146
|
+
*/
|
|
147
|
+
signedUrl(path: string, options?: SignedFileOptions): Promise<string>;
|
|
148
|
+
/**
|
|
149
|
+
* A URL the browser can `PUT` the file to directly, so the bytes never transit
|
|
150
|
+
* your app — the point of this on the edge, where proxying a large upload
|
|
151
|
+
* through a Worker is exactly what you don't want.
|
|
152
|
+
*
|
|
153
|
+
* Only the backend can accept such a write, so this requires a disk that
|
|
154
|
+
* implements `signedUploadUrl` (see the S3/R2 recipe in the storage guide).
|
|
155
|
+
*/
|
|
156
|
+
signedUploadUrl(path: string, options?: SignedUploadOptions): Promise<string>;
|
|
51
157
|
/** The underlying driver, for backend-specific operations. */
|
|
52
158
|
get driver(): Disk;
|
|
53
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* A `Storage` backed by a `MemoryDisk`, with assertions — what `fakeDisk()`
|
|
162
|
+
* installs so tests never touch a real bucket.
|
|
163
|
+
*/
|
|
164
|
+
export declare class FakeStorage extends Storage {
|
|
165
|
+
constructor();
|
|
166
|
+
assertExists(path: string): Promise<void>;
|
|
167
|
+
assertMissing(path: string): Promise<void>;
|
|
168
|
+
/** Assert a file's contents, as UTF-8 text. */
|
|
169
|
+
assertContents(path: string, expected: string): Promise<void>;
|
|
170
|
+
/** Assert how many files are stored, optionally under a prefix. */
|
|
171
|
+
assertCount(expected: number, prefix?: string): Promise<void>;
|
|
172
|
+
}
|
|
54
173
|
/** Register a disk, optionally under a name (default: `"default"`). */
|
|
55
174
|
export declare function setDisk(disk: Disk, name?: string): Storage;
|
|
56
175
|
/** The default disk, or a named one registered with `setDisk(disk, name)`. */
|
|
57
176
|
export declare function storage(name?: string): Storage;
|
|
177
|
+
/**
|
|
178
|
+
* Swap a disk for an in-memory `FakeStorage` so tests never touch a real bucket,
|
|
179
|
+
* and assert against what was written. Undo with `restoreDisk()`.
|
|
180
|
+
*
|
|
181
|
+
* const disk = fakeDisk();
|
|
182
|
+
* await request.post("/avatars", form);
|
|
183
|
+
* await disk.assertExists("avatars/1.png");
|
|
184
|
+
*/
|
|
185
|
+
export declare function fakeDisk(name?: string): FakeStorage;
|
|
186
|
+
/** Restore the real disk after `fakeDisk()`. With no name, restores them all. */
|
|
187
|
+
export declare function restoreDisk(name?: string): void;
|
|
188
|
+
export interface ServeStorageOptions {
|
|
189
|
+
/** Which registered disk to serve from. Default: `"default"`. */
|
|
190
|
+
disk?: string;
|
|
191
|
+
/** URL prefix the files live under. Default: `"/storage"`. */
|
|
192
|
+
basePath?: string;
|
|
193
|
+
/**
|
|
194
|
+
* Require a valid signature from `signedUrl()` — the private-file mode.
|
|
195
|
+
* Unsigned or expired requests get a 403. Default: false.
|
|
196
|
+
*/
|
|
197
|
+
signed?: boolean;
|
|
198
|
+
/** `Cache-Control` max-age in seconds. Omit for no header. */
|
|
199
|
+
maxAge?: number;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Serve files from a disk over HTTP — what makes the fallback `signedUrl()` real
|
|
203
|
+
* for disks without backend presigning (the memory disk, a local-filesystem
|
|
204
|
+
* disk). Requests that don't match `basePath`, or that name a file the disk
|
|
205
|
+
* doesn't have, fall through to your routes.
|
|
206
|
+
*
|
|
207
|
+
* this.use(serveStorage()); // public files
|
|
208
|
+
* this.use(serveStorage({ basePath: "/private", signed: true }));
|
|
209
|
+
*/
|
|
210
|
+
export declare function serveStorage(options?: ServeStorageOptions): MiddlewareHandler;
|