@mybase-dev/sdk 1.0.0 → 1.0.2
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/README.md +147 -34
- package/dist/analytics-script.d.ts +3 -0
- package/dist/better-auth.d.ts +123 -0
- package/dist/better-auth.js +81 -0
- package/dist/captcha-widget.d.ts +1 -0
- package/dist/captcha.d.ts +54 -0
- package/dist/captcha.js +239 -0
- package/dist/captcha.min.js +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/search.d.ts +120 -0
- package/dist/search.js +315 -0
- package/dist/secrets.d.ts +138 -0
- package/dist/secrets.js +274 -0
- package/dist/vector.d.ts +234 -0
- package/dist/vector.js +331 -0
- package/package.json +33 -3
- package/src/better-auth.ts +189 -0
- package/src/captcha.ts +110 -0
- package/src/config.ts +10 -3
- package/src/search.ts +250 -0
- package/src/secrets.ts +234 -0
- package/src/vector.ts +356 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mybase-dev/sdk
|
|
2
2
|
|
|
3
|
-
Helpers for apps that run on [mybase](https://github.com/
|
|
3
|
+
Helpers for apps that run on [mybase](https://github.com/LandonHarter/mybase), a self-hosted backend.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install @mybase-dev/sdk # or bun add / pnpm add
|
|
@@ -8,6 +8,31 @@ npm install @mybase-dev/sdk # or bun add / pnpm add
|
|
|
8
8
|
|
|
9
9
|
No runtime dependencies. Uses Web Crypto, so it works in Node 20+, Bun, Deno, Next.js route handlers, Hono, and edge runtimes.
|
|
10
10
|
|
|
11
|
+
| Import | Use |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `@mybase-dev/sdk/analytics` | Record page views, custom events, and errors |
|
|
14
|
+
| `@mybase-dev/sdk/cache` | Read and write the project's cache over HTTP |
|
|
15
|
+
| `@mybase-dev/sdk/ratelimit` | Check rate limits |
|
|
16
|
+
| `@mybase-dev/sdk/queue` | Send, receive, and consume queue messages |
|
|
17
|
+
| `@mybase-dev/sdk/search` | Search the web and read pages, plus tools for LLM agents |
|
|
18
|
+
| `@mybase-dev/sdk/vector` | Vector, full-text, and hybrid search over documents |
|
|
19
|
+
| `@mybase-dev/sdk/cron` | Verify cron calls, handle jobs, and schedule one-off tasks |
|
|
20
|
+
| `@mybase-dev/sdk/better-auth` | Better Auth on mybase: database URL, session storage, audit plugin, captcha |
|
|
21
|
+
| `@mybase-dev/sdk/captcha` | Solve captcha challenges in the browser, verify tokens on the server |
|
|
22
|
+
| `@mybase-dev/sdk/config` | Declare cron jobs, rate limit rules, queues, and vector indexes in `mybase.config.ts` |
|
|
23
|
+
|
|
24
|
+
## API keys
|
|
25
|
+
|
|
26
|
+
Cache, rate limits, queues, and one-off tasks authenticate with a mybase API key (`mb_…`). Each key has per-service permissions (`cache:write`, `ratelimit:write`, `queue:write`, `cron:write`, … or `admin`) and can be limited to some projects, so one key can cover everything an app uses. Create one in the dashboard (Settings → API keys) or with the CLI:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
mybase token create shop-app --project shop --perm cache:write,ratelimit:write,queue:write,cron:write
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Put it in your app's server-side environment as `MYBASE_API_KEY`. Never expose it to browser code: analytics has its own public write key for that.
|
|
33
|
+
|
|
34
|
+
Getting started guide: [docs/getting-started.md](docs/getting-started.md). Your mybase instance also serves every guide at `/docs` and `/llms.txt`.
|
|
35
|
+
|
|
11
36
|
## Analytics (`@mybase-dev/sdk/analytics`)
|
|
12
37
|
|
|
13
38
|
Page views, custom events, and errors, shown on the project's Analytics page. Turn analytics on for the project (dashboard, or `mybase analytics enable <project>`) to get its public write key, then:
|
|
@@ -51,7 +76,7 @@ Full guide: [docs/cache.md](docs/cache.md).
|
|
|
51
76
|
|
|
52
77
|
## Rate limits (`@mybase-dev/sdk/ratelimit`)
|
|
53
78
|
|
|
54
|
-
Limits shared by every instance of your app. Turn them on (dashboard, or `mybase ratelimit enable <project>`),
|
|
79
|
+
Limits shared by every instance of your app. Turn them on (dashboard, or `mybase ratelimit enable <project>`), use an API key with `ratelimit:write`, define rules on the dashboard or in [`mybase.config.ts`](#define-jobs-rules-and-queues-in-code), then:
|
|
55
80
|
|
|
56
81
|
```ts
|
|
57
82
|
import { createRateLimiter, rateLimitHeaders } from "@mybase-dev/sdk/ratelimit";
|
|
@@ -65,7 +90,7 @@ If mybase can't be reached, it allows the request (pass `failClosed: true` to de
|
|
|
65
90
|
|
|
66
91
|
## Queues (`@mybase-dev/sdk/queue`)
|
|
67
92
|
|
|
68
|
-
SQS-style standard and FIFO queues. Turn them on (dashboard, or `mybase queue enable <project>`),
|
|
93
|
+
SQS-style standard and FIFO queues. Turn them on (dashboard, or `mybase queue enable <project>`), use an API key with `queue:write`, create queues on the dashboard, in [`mybase.config.ts`](#define-jobs-rules-and-queues-in-code), or with `queue.createQueue(...)`, then:
|
|
69
94
|
|
|
70
95
|
```ts
|
|
71
96
|
import { createQueueClient } from "@mybase-dev/sdk/queue";
|
|
@@ -75,7 +100,73 @@ await queue.send("emails", { to: "ada@example.com" });
|
|
|
75
100
|
queue.consume("emails", async (message) => sendEmail(message.json()));
|
|
76
101
|
```
|
|
77
102
|
|
|
78
|
-
Delivery is at least once; failed messages are retried and can go to a dead-letter queue. Full guide: [docs/queues.md](docs/queues.md).
|
|
103
|
+
Delivery is at least once; `consume` deletes a message when its handler resolves, failed messages are retried and can go to a dead-letter queue. Full guide: [docs/queues.md](docs/queues.md).
|
|
104
|
+
|
|
105
|
+
## Web search (`@mybase-dev/sdk/search`)
|
|
106
|
+
|
|
107
|
+
Web search and page reading for your app's AI agent, through mybase's own SearXNG (no search API to pay for). Turn it on (dashboard, or `mybase search enable <project>`), use an API key with `search:read`, then:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { createSearchClient, searchTools } from "@mybase-dev/sdk/search";
|
|
111
|
+
|
|
112
|
+
const web = createSearchClient({ apiUrl: "https://mybase.example.com", project: "shop", apiKey: process.env.MYBASE_API_KEY! });
|
|
113
|
+
const { results } = await web.search("bun sql postgres");
|
|
114
|
+
const page = await web.fetch(results[0]!.url); // main content as markdown
|
|
115
|
+
const tools = searchTools(web); // web_search + web_fetch for an LLM tool-use loop
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Full guide: [docs/search.md](docs/search.md).
|
|
119
|
+
|
|
120
|
+
## Vector search (`@mybase-dev/sdk/vector`)
|
|
121
|
+
|
|
122
|
+
Index documents and search them by meaning (pgvector), keywords (Postgres full-text search), or both (hybrid). They live in your project's own database. Turn it on (dashboard, or `mybase vector enable <project>`), use an API key with `vector:write` (`vector:read` to only search), then:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { createVectorClient, retrievalTool } from "@mybase-dev/sdk/vector";
|
|
126
|
+
|
|
127
|
+
const vector = createVectorClient({ apiUrl: "https://mybase.example.com", project: "shop", apiKey: process.env.MYBASE_API_KEY! });
|
|
128
|
+
const docs = vector.index<{ lang: string }>("docs");
|
|
129
|
+
await docs.upsert([{ id: "faq-1", title: "Reset your password", text: "…", metadata: { lang: "en" } }]);
|
|
130
|
+
const { matches } = await docs.query("forgot my password", { topK: 5, filter: { lang: "en" } });
|
|
131
|
+
const tool = retrievalTool(docs, { about: "the help center" }); // for an LLM tool-use loop
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Send your own vectors, or set an embedding model for the project (any OpenAI-compatible endpoint) and mybase embeds text for you. Full guide: [docs/vector.md](docs/vector.md).
|
|
135
|
+
|
|
136
|
+
## Better Auth (`@mybase-dev/sdk/better-auth`)
|
|
137
|
+
|
|
138
|
+
Sign your app's users in with [Better Auth](https://better-auth.com), backed by mybase. Turn on the users service (`mybase users enable <project>`) and the cache, use an API key with `users:write` and `cache:write`, then:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { betterAuth } from "better-auth";
|
|
142
|
+
import { Pool } from "pg";
|
|
143
|
+
import { createCache } from "@mybase-dev/sdk/cache";
|
|
144
|
+
import { authDatabaseUrl, mybaseAuth, mybaseSecondaryStorage } from "@mybase-dev/sdk/better-auth";
|
|
145
|
+
|
|
146
|
+
const mybase = { apiUrl: "https://mybase.example.com", project: "shop", apiKey: process.env.MYBASE_API_KEY! };
|
|
147
|
+
export const auth = betterAuth({
|
|
148
|
+
database: new Pool({ connectionString: authDatabaseUrl(process.env.DATABASE_URL!) }), // tables in the "auth" schema
|
|
149
|
+
secondaryStorage: mybaseSecondaryStorage(createCache(mybase)), // sessions in the mybase cache
|
|
150
|
+
session: { storeSessionInDatabase: true }, // so the dashboard can list and revoke them
|
|
151
|
+
emailAndPassword: { enabled: true },
|
|
152
|
+
plugins: [mybaseAuth(mybase)], // sign-ins in the audit log
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
No dependency on Better Auth itself. Full guide: [docs/better-auth.md](docs/better-auth.md).
|
|
157
|
+
|
|
158
|
+
## Captcha (`@mybase-dev/sdk/captcha`)
|
|
159
|
+
|
|
160
|
+
Self-hosted, proof-of-work bot checks: no puzzles, cookies, or third party. Turn it on (`mybase captcha enable <project>`), solve in the browser, verify on the server with an API key that has `captcha:write`:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { createCaptchaClient, solveCaptcha } from "@mybase-dev/sdk/captcha";
|
|
164
|
+
|
|
165
|
+
const token = await solveCaptcha({ apiUrl: "https://mybase.example.com", project: "shop" }); // browser
|
|
166
|
+
const { success } = await createCaptchaClient({ apiUrl, project: "shop", apiKey }).verify(token); // server
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Or the script tag `<script defer src="https://mybase.example.com/captcha.js" data-project="shop">` on forms marked `data-mybase-captcha`. With Better Auth: `captcha(mybaseCaptcha({ …, secretKey }))`. Full guide: [docs/captcha.md](docs/captcha.md).
|
|
79
170
|
|
|
80
171
|
## Cron (`@mybase-dev/sdk/cron`)
|
|
81
172
|
|
|
@@ -101,6 +192,8 @@ Responses: `200` when the job finishes, `401` for a bad or stale signature, `404
|
|
|
101
192
|
|
|
102
193
|
**Rotating the secret:** pass both secrets, new first, while you deploy: `secret: [process.env.MYBASE_CRON_SECRET!, process.env.MYBASE_CRON_SECRET_OLD!]`.
|
|
103
194
|
|
|
195
|
+
Full guide: [docs/cron-handler.md](docs/cron-handler.md). Jobs are created on the dashboard, with `mybase cron create`, or [in code](#define-jobs-rules-and-queues-in-code).
|
|
196
|
+
|
|
104
197
|
### Lower level
|
|
105
198
|
|
|
106
199
|
```ts
|
|
@@ -113,35 +206,6 @@ const run = await verifyCronRequest(request, process.env.MYBASE_CRON_SECRET!);
|
|
|
113
206
|
await verifyCronSignature({ body: req.body, signature: req.get("X-Mybase-Signature"), secret });
|
|
114
207
|
```
|
|
115
208
|
|
|
116
|
-
### Define jobs in code
|
|
117
|
-
|
|
118
|
-
Declare jobs next to your app and push them with the mybase CLI. Keep this file free of app code: the CLI imports only this file.
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
// mybase.config.ts
|
|
122
|
-
import { defineConfig } from "@mybase-dev/sdk/config";
|
|
123
|
-
|
|
124
|
-
export default defineConfig({
|
|
125
|
-
project: "shop",
|
|
126
|
-
cron: {
|
|
127
|
-
baseUrl: "https://shop.example.com", // must be reachable from the mybase server
|
|
128
|
-
jobs: {
|
|
129
|
-
cleanup: { schedule: "0 3 * * *" }, // POSTs to baseUrl + /api/cron
|
|
130
|
-
digest: { schedule: "0 8 * * 1", timezone: "America/Chicago", path: "/api/cron", payload: { kind: "weekly" } },
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
```sh
|
|
137
|
-
mybase cron sync --dry-run # show what would change
|
|
138
|
-
mybase cron sync # create, update, and delete code-defined jobs to match
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Jobs created in the dashboard are never touched by a sync. For typed job names in the handler, pass the config type: `cronHandler<typeof config>({ ... })`.
|
|
142
|
-
|
|
143
|
-
Job options: `schedule` (5-field cron, minute precision), `path` (default `/api/cron`), `timezone` (IANA, default `UTC`), `payload` (JSON, up to 16 KB), `retries` (0–5, default 2), `timeoutMs` (1000–120000, default 30000).
|
|
144
|
-
|
|
145
209
|
### One-off tasks
|
|
146
210
|
|
|
147
211
|
Schedule a single call to a handler from your app code, e.g. a reminder a day after an order. The task calls the same `cronHandler` as cron jobs: `run.job` is the name you pass, `run.trigger` is `"task"`, and `run.runId` is the task id.
|
|
@@ -185,7 +249,7 @@ export const POST = cronHandler({
|
|
|
185
249
|
- Finished and cancelled tasks are kept for 7 days. A project can have 10,000 pending tasks.
|
|
186
250
|
- Errors from the API throw `MybaseApiError` (with `status`); `get`/`cancel` resolve to `null` for an unknown task.
|
|
187
251
|
|
|
188
|
-
The client sends an API key with `cron:write` (not an admin one) as `Authorization: Bearer`.
|
|
252
|
+
The client sends an API key with `cron:write` (not an admin one) as `Authorization: Bearer`. Full guide: [docs/one-off-tasks.md](docs/one-off-tasks.md).
|
|
189
253
|
|
|
190
254
|
### Signature format
|
|
191
255
|
|
|
@@ -204,3 +268,52 @@ Reject the call if `t` is more than 5 minutes from now, then compare `v1` in con
|
|
|
204
268
|
`X-Mybase-Job` and `X-Mybase-Run` headers carry the job name and run id too. Task calls add `"key"`, and `trigger` is `"task"`.
|
|
205
269
|
|
|
206
270
|
Requests from your app to the mybase API (`createCronClient`) carry an API key as `Authorization: Bearer`. Routes: `POST /cron/<project>/tasks`, `GET|DELETE /cron/<project>/tasks/<id>`, `GET|DELETE /cron/<project>/tasks/by-key/<key>`.
|
|
271
|
+
|
|
272
|
+
Full reference: [docs/signatures.md](docs/signatures.md).
|
|
273
|
+
|
|
274
|
+
## Define jobs, rules, and queues in code
|
|
275
|
+
|
|
276
|
+
Declare cron jobs, rate limit rules, and queues next to your app and push them with the mybase CLI. Keep this file free of app code: the CLI imports only this file.
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
// mybase.config.ts
|
|
280
|
+
import { defineConfig } from "@mybase-dev/sdk/config";
|
|
281
|
+
|
|
282
|
+
export default defineConfig({
|
|
283
|
+
project: "shop",
|
|
284
|
+
cron: {
|
|
285
|
+
baseUrl: "https://shop.example.com", // must be reachable from the mybase server
|
|
286
|
+
jobs: {
|
|
287
|
+
cleanup: { schedule: "0 3 * * *" }, // POSTs to baseUrl + /api/cron
|
|
288
|
+
digest: { schedule: "0 8 * * 1", timezone: "America/Chicago", path: "/api/cron", payload: { kind: "weekly" } },
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
ratelimit: {
|
|
292
|
+
rules: {
|
|
293
|
+
login: { limit: 5, window: "1m" },
|
|
294
|
+
api: { algorithm: "token", limit: 100, window: "1m", burst: 200 },
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
queue: {
|
|
298
|
+
queues: {
|
|
299
|
+
"emails-dlq": {},
|
|
300
|
+
emails: { visibilityTimeout: "2m", deadLetter: { queue: "emails-dlq", maxReceives: 5 } },
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```sh
|
|
307
|
+
mybase cron sync --dry-run # show what would change
|
|
308
|
+
mybase cron sync # create, update, and delete code-defined jobs to match
|
|
309
|
+
mybase ratelimit sync # same for rate limit rules
|
|
310
|
+
mybase queue sync # same for queues (asks before deleting a queue: its messages go too)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Each sync only touches what came from code; jobs, rules, and queues created on the dashboard are left alone. For typed job names in the handler, pass the config type: `cronHandler<typeof config>({ ... })`.
|
|
314
|
+
|
|
315
|
+
- **Job options:** `schedule` (5-field cron, minute precision), `path` (default `/api/cron`), `timezone` (IANA, default `UTC`), `payload` (JSON, up to 16 KB), `retries` (0–5, default 2), `timeoutMs` (1000–120000, default 30000).
|
|
316
|
+
- **Rule options:** `algorithm` (`sliding` default, `fixed`, `token`), `limit`, `window` (ms or `"10s"`, `"1m"`, `"1h"`, `"1d"`; 1s–30d), `burst` (token bucket only).
|
|
317
|
+
- **Queue options:** `fifo` (fixed at creation), `visibilityTimeout` (default 30s), `delay`, `retention` (default 4d), `receiveWait`, `maxMessageBytes`, `contentDedup` (FIFO), `deadLetter` (`{ queue, maxReceives }`).
|
|
318
|
+
|
|
319
|
+
Full guide: [docs/jobs-in-code.md](docs/jobs-in-code.md).
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Cache } from "./cache";
|
|
2
|
+
/** Better Auth's tables live in this schema of the project's database (the `users` service creates it). */
|
|
3
|
+
export declare const AUTH_SCHEMA = "auth";
|
|
4
|
+
/**
|
|
5
|
+
* The project's connection string with `search_path` set to the auth schema, so Better Auth's
|
|
6
|
+
* tables (and its migrations) land there instead of next to your app's tables in `public`.
|
|
7
|
+
* Pass `extra` schemas to also see them unqualified (e.g. `["public"]`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function authDatabaseUrl(url: string, extra?: string[]): string;
|
|
10
|
+
/** What Better Auth's `secondaryStorage` option takes. TTLs are in seconds. */
|
|
11
|
+
export interface SecondaryStorage {
|
|
12
|
+
get(key: string): Promise<string | null>;
|
|
13
|
+
getAndDelete(key: string): Promise<string | null>;
|
|
14
|
+
increment(key: string, ttl: number): Promise<number>;
|
|
15
|
+
set(key: string, value: string, ttl?: number): Promise<void>;
|
|
16
|
+
delete(key: string): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Better Auth's `secondaryStorage` on the project's mybase cache (Valkey): sessions and rate
|
|
20
|
+
* limit counters stay in memory instead of hitting Postgres (and the SD card) on every request.
|
|
21
|
+
* Keys go under `<project>:auth:`, inside the project's cache ACL. Needs the cache service and an
|
|
22
|
+
* API key with `cache:write`. The dashboard's Users page revokes sessions here too.
|
|
23
|
+
*/
|
|
24
|
+
export declare function mybaseSecondaryStorage(cache: Cache, { prefix }?: {
|
|
25
|
+
prefix?: string;
|
|
26
|
+
}): SecondaryStorage;
|
|
27
|
+
export interface MybaseAuthOptions {
|
|
28
|
+
/** Where the mybase API is reachable from your app. */
|
|
29
|
+
apiUrl: string;
|
|
30
|
+
project: string;
|
|
31
|
+
/** A mybase API key with `users:write` for this project. Server-side only. */
|
|
32
|
+
apiKey: string;
|
|
33
|
+
/** Default `globalThis.fetch`. */
|
|
34
|
+
fetch?: typeof fetch;
|
|
35
|
+
/** Called when an event can't be sent (it's dropped, never retried). Default: console.warn. */
|
|
36
|
+
onError?: (err: unknown) => void;
|
|
37
|
+
}
|
|
38
|
+
/** What the app reports to mybase's audit log (`POST /users/:project/events`). */
|
|
39
|
+
export type UserEventType = "user.created" | "user.deleted" | "session.created" | "session.revoked";
|
|
40
|
+
export interface UserEvent {
|
|
41
|
+
type: UserEventType;
|
|
42
|
+
userId: string;
|
|
43
|
+
/** The end user's IP and user agent, as Better Auth saw them. */
|
|
44
|
+
ip?: string | null;
|
|
45
|
+
userAgent?: string | null;
|
|
46
|
+
}
|
|
47
|
+
interface SessionLike {
|
|
48
|
+
userId: string;
|
|
49
|
+
ipAddress?: string | null;
|
|
50
|
+
userAgent?: string | null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A Better Auth plugin that reports sign-ups, sign-ins, sign-outs, and deleted users to the
|
|
54
|
+
* project's mybase audit log, where they show up next to everything else (and on the
|
|
55
|
+
* dashboard's Users page). No passwords, emails, or tokens are sent: user ids, IPs, user agents.
|
|
56
|
+
* Sending never blocks or fails a sign-in.
|
|
57
|
+
*/
|
|
58
|
+
export declare function mybaseAuth(options: MybaseAuthOptions): {
|
|
59
|
+
id: "mybase";
|
|
60
|
+
init(): {
|
|
61
|
+
options: {
|
|
62
|
+
databaseHooks: {
|
|
63
|
+
user: {
|
|
64
|
+
create: {
|
|
65
|
+
after: (user: {
|
|
66
|
+
id: string;
|
|
67
|
+
}) => Promise<void>;
|
|
68
|
+
};
|
|
69
|
+
delete: {
|
|
70
|
+
after: (user: {
|
|
71
|
+
id: string;
|
|
72
|
+
}) => Promise<void>;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
session: {
|
|
76
|
+
create: {
|
|
77
|
+
after: (session: SessionLike) => Promise<void>;
|
|
78
|
+
};
|
|
79
|
+
delete: {
|
|
80
|
+
after: (session: SessionLike) => Promise<void>;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
export interface MybaseCaptchaOptions {
|
|
88
|
+
apiUrl: string;
|
|
89
|
+
project: string;
|
|
90
|
+
/** The project's captcha secret (dashboard → Captcha, or `mybase captcha key`). Server-side only. */
|
|
91
|
+
secretKey: string;
|
|
92
|
+
/** Better Auth routes that need a solved captcha. Default: sign-up, sign-in, and password reset. */
|
|
93
|
+
endpoints?: string[];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Options for Better Auth's `captcha()` plugin that check tokens with mybase's self-hosted
|
|
97
|
+
* captcha instead of Cloudflare's: mybase answers in Turnstile's siteverify format. Put the
|
|
98
|
+
* widget (`<script src="<mybase>/captcha.js">`) on your forms and send its token in the
|
|
99
|
+
* `x-captcha-response` header, as Better Auth expects.
|
|
100
|
+
*/
|
|
101
|
+
export declare function mybaseCaptcha(options: MybaseCaptchaOptions): {
|
|
102
|
+
endpoints?: string[] | undefined;
|
|
103
|
+
provider: "cloudflare-turnstile";
|
|
104
|
+
secretKey: string;
|
|
105
|
+
siteVerifyURLOverride: string;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* The statement that tells Postgres who the signed-in user is, for row-level security policies
|
|
109
|
+
* on `mybase_auth.uid()`. Run it inside the same transaction as your queries:
|
|
110
|
+
*
|
|
111
|
+
* await sql.begin(async (tx) => {
|
|
112
|
+
* const q = userContextQuery(session.user.id);
|
|
113
|
+
* await tx.unsafe(q.text, q.values);
|
|
114
|
+
* return tx`select * from orders`; // policy: using (user_id = mybase_auth.uid())
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* Policies only apply to the table owner (your app's role) with `force row level security`.
|
|
118
|
+
*/
|
|
119
|
+
export declare function userContextQuery(userId: string): {
|
|
120
|
+
text: string;
|
|
121
|
+
values: [string];
|
|
122
|
+
};
|
|
123
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// src/better-auth.ts
|
|
2
|
+
var AUTH_SCHEMA = "auth";
|
|
3
|
+
function authDatabaseUrl(url, extra = []) {
|
|
4
|
+
const u = new URL(url);
|
|
5
|
+
const path = [AUTH_SCHEMA, ...extra].join(",");
|
|
6
|
+
u.searchParams.set("options", `-c search_path=${path}`);
|
|
7
|
+
return u.toString();
|
|
8
|
+
}
|
|
9
|
+
function mybaseSecondaryStorage(cache, { prefix = "auth:" } = {}) {
|
|
10
|
+
const k = (key) => cache.key(prefix + key);
|
|
11
|
+
return {
|
|
12
|
+
get: (key) => cache.command(["GET", k(key)]),
|
|
13
|
+
getAndDelete: (key) => cache.command(["GETDEL", k(key)]),
|
|
14
|
+
async increment(key, ttl) {
|
|
15
|
+
const [, incr] = await cache.pipeline().command(["SET", k(key), 0, "EX", Math.max(1, Math.ceil(ttl)), "NX"]).command(["INCR", k(key)]).exec({ atomic: true });
|
|
16
|
+
if (!incr || "error" in incr)
|
|
17
|
+
throw new Error(`mybase cache: ${incr && "error" in incr ? incr.error : "no reply"}`);
|
|
18
|
+
return Number(incr.result);
|
|
19
|
+
},
|
|
20
|
+
async set(key, value, ttl) {
|
|
21
|
+
await cache.command(ttl && ttl > 0 ? ["SET", k(key), value, "EX", Math.ceil(ttl)] : ["SET", k(key), value]);
|
|
22
|
+
},
|
|
23
|
+
async delete(key) {
|
|
24
|
+
await cache.command(["DEL", k(key)]);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function mybaseAuth(options) {
|
|
29
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
30
|
+
const url = `${options.apiUrl.replace(/\/+$/, "")}/users/${encodeURIComponent(options.project)}/events`;
|
|
31
|
+
const onError = options.onError ?? ((err) => console.warn("mybase: auth event not sent:", err));
|
|
32
|
+
const send = (event) => {
|
|
33
|
+
doFetch(url, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
body: JSON.stringify({ events: [event] }),
|
|
36
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${options.apiKey}` }
|
|
37
|
+
}).then(async (res) => {
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
onError(new Error(`mybase API answered ${res.status}: ${await res.text().catch(() => "")}`));
|
|
40
|
+
}).catch(onError);
|
|
41
|
+
};
|
|
42
|
+
const fromSession = (type, s) => send({ type, userId: s.userId, ip: s.ipAddress ?? null, userAgent: s.userAgent ?? null });
|
|
43
|
+
return {
|
|
44
|
+
id: "mybase",
|
|
45
|
+
init() {
|
|
46
|
+
return {
|
|
47
|
+
options: {
|
|
48
|
+
databaseHooks: {
|
|
49
|
+
user: {
|
|
50
|
+
create: { after: async (user) => send({ type: "user.created", userId: user.id }) },
|
|
51
|
+
delete: { after: async (user) => send({ type: "user.deleted", userId: user.id }) }
|
|
52
|
+
},
|
|
53
|
+
session: {
|
|
54
|
+
create: { after: async (session) => fromSession("session.created", session) },
|
|
55
|
+
delete: { after: async (session) => fromSession("session.revoked", session) }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function mybaseCaptcha(options) {
|
|
64
|
+
return {
|
|
65
|
+
provider: "cloudflare-turnstile",
|
|
66
|
+
secretKey: options.secretKey,
|
|
67
|
+
siteVerifyURLOverride: `${options.apiUrl.replace(/\/+$/, "")}/captcha/${encodeURIComponent(options.project)}/siteverify`,
|
|
68
|
+
...options.endpoints ? { endpoints: options.endpoints } : {}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function userContextQuery(userId) {
|
|
72
|
+
return { text: "select set_config('mybase.user_id', $1, true)", values: [userId] };
|
|
73
|
+
}
|
|
74
|
+
export {
|
|
75
|
+
AUTH_SCHEMA,
|
|
76
|
+
authDatabaseUrl,
|
|
77
|
+
mybaseAuth,
|
|
78
|
+
mybaseCaptcha,
|
|
79
|
+
mybaseSecondaryStorage,
|
|
80
|
+
userContextQuery
|
|
81
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { MybaseApiError } from "./cron";
|
|
2
|
+
export { MybaseApiError };
|
|
3
|
+
export declare const CAPTCHA_LIMITS: {
|
|
4
|
+
/** How long a challenge (and so its token) stays valid. */
|
|
5
|
+
readonly expiresMs: number;
|
|
6
|
+
/** The work: the browser tries up to this many numbers (half on average). */
|
|
7
|
+
readonly maxNumber: {
|
|
8
|
+
readonly min: 1000;
|
|
9
|
+
readonly max: 1000000;
|
|
10
|
+
readonly default: 50000;
|
|
11
|
+
};
|
|
12
|
+
readonly tokenLength: 2000;
|
|
13
|
+
};
|
|
14
|
+
/** `GET /captcha/:project/challenge`. `salt` carries its expiry (`…?expires=<unix seconds>`). */
|
|
15
|
+
export interface CaptchaChallenge {
|
|
16
|
+
algorithm: "SHA-256";
|
|
17
|
+
challenge: string;
|
|
18
|
+
salt: string;
|
|
19
|
+
maxnumber: number;
|
|
20
|
+
signature: string;
|
|
21
|
+
}
|
|
22
|
+
/** What `verify` answers. `error` says why a token was refused. */
|
|
23
|
+
export interface CaptchaVerifyResponse {
|
|
24
|
+
success: boolean;
|
|
25
|
+
error?: "invalid" | "expired" | "used" | "unavailable";
|
|
26
|
+
}
|
|
27
|
+
export declare function sha256Hex(text: string): Promise<string>;
|
|
28
|
+
/** The token for a solved challenge: base64 of the challenge plus the number found. */
|
|
29
|
+
export declare function encodeToken(c: CaptchaChallenge, number: number): string;
|
|
30
|
+
/** Finds the number. Rejects if none up to `maxnumber` matches, or when `signal` aborts. */
|
|
31
|
+
export declare function solveChallenge(c: CaptchaChallenge, { signal }?: {
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
}): Promise<string>;
|
|
34
|
+
export interface SolveOptions {
|
|
35
|
+
/** Where the mybase API is reachable from the browser. */
|
|
36
|
+
apiUrl: string;
|
|
37
|
+
project: string;
|
|
38
|
+
fetch?: typeof fetch;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
/** Fetches a challenge and solves it. Returns the token to send with the form. */
|
|
42
|
+
export declare function solveCaptcha(options: SolveOptions): Promise<string>;
|
|
43
|
+
export interface CaptchaClientOptions {
|
|
44
|
+
apiUrl: string;
|
|
45
|
+
project: string;
|
|
46
|
+
/** A mybase API key with `captcha:write` for this project. Server-side only. */
|
|
47
|
+
apiKey: string;
|
|
48
|
+
fetch?: typeof fetch;
|
|
49
|
+
}
|
|
50
|
+
export interface CaptchaClient {
|
|
51
|
+
/** Checks a token once. A second check of the same token fails with `used`. */
|
|
52
|
+
verify(token: string): Promise<CaptchaVerifyResponse>;
|
|
53
|
+
}
|
|
54
|
+
export declare function createCaptchaClient(options: CaptchaClientOptions): CaptchaClient;
|