tinbase 0.1.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/LICENSE +21 -0
- package/README.md +185 -0
- package/assets/logo.svg +18 -0
- package/dist/auth/handler.d.ts +43 -0
- package/dist/auth/handler.js +291 -0
- package/dist/auth/handler.js.map +1 -0
- package/dist/auth/password.d.ts +3 -0
- package/dist/auth/password.js +40 -0
- package/dist/auth/password.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +181 -0
- package/dist/cli.js.map +1 -0
- package/dist/db/bootstrap.d.ts +6 -0
- package/dist/db/bootstrap.js +213 -0
- package/dist/db/bootstrap.js.map +1 -0
- package/dist/db/database.d.ts +83 -0
- package/dist/db/database.js +270 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/engine.d.ts +30 -0
- package/dist/db/engine.js +28 -0
- package/dist/db/engine.js.map +1 -0
- package/dist/db/pglite-engine.d.ts +3 -0
- package/dist/db/pglite-engine.js +39 -0
- package/dist/db/pglite-engine.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +145 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +20 -0
- package/dist/jwt.js +72 -0
- package/dist/jwt.js.map +1 -0
- package/dist/node/bun-server.d.ts +8 -0
- package/dist/node/bun-server.js +41 -0
- package/dist/node/bun-server.js.map +1 -0
- package/dist/node/fs-driver.d.ts +11 -0
- package/dist/node/fs-driver.js +39 -0
- package/dist/node/fs-driver.js.map +1 -0
- package/dist/node/index.d.ts +7 -0
- package/dist/node/index.js +8 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/native/engine.d.ts +13 -0
- package/dist/node/native/engine.js +186 -0
- package/dist/node/native/engine.js.map +1 -0
- package/dist/node/native/wire.d.ts +38 -0
- package/dist/node/native/wire.js +345 -0
- package/dist/node/native/wire.js.map +1 -0
- package/dist/node/project.d.ts +6 -0
- package/dist/node/project.js +29 -0
- package/dist/node/project.js.map +1 -0
- package/dist/node/server-shared.d.ts +6 -0
- package/dist/node/server-shared.js +2 -0
- package/dist/node/server-shared.js.map +1 -0
- package/dist/node/server.d.ts +14 -0
- package/dist/node/server.js +83 -0
- package/dist/node/server.js.map +1 -0
- package/dist/node/ws.d.ts +8 -0
- package/dist/node/ws.js +165 -0
- package/dist/node/ws.js.map +1 -0
- package/dist/realtime/engine.d.ts +53 -0
- package/dist/realtime/engine.js +437 -0
- package/dist/realtime/engine.js.map +1 -0
- package/dist/rest/build.d.ts +53 -0
- package/dist/rest/build.js +505 -0
- package/dist/rest/build.js.map +1 -0
- package/dist/rest/errors.d.ts +2 -0
- package/dist/rest/errors.js +48 -0
- package/dist/rest/errors.js.map +1 -0
- package/dist/rest/handler.d.ts +11 -0
- package/dist/rest/handler.js +332 -0
- package/dist/rest/handler.js.map +1 -0
- package/dist/rest/parse.d.ts +69 -0
- package/dist/rest/parse.js +261 -0
- package/dist/rest/parse.js.map +1 -0
- package/dist/storage/driver.d.ts +9 -0
- package/dist/storage/driver.js +18 -0
- package/dist/storage/driver.js.map +1 -0
- package/dist/storage/handler.d.ts +49 -0
- package/dist/storage/handler.js +534 -0
- package/dist/storage/handler.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sanket Sahu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<p align="center"><img src="assets/logo.svg" width="96" alt="tinbase logo"></p>
|
|
2
|
+
|
|
3
|
+
# tinbase
|
|
4
|
+
|
|
5
|
+
A pure-JS, Docker-free Supabase backend built on [PGlite](https://pglite.dev) (Postgres compiled to WASM). It speaks the same wire protocols as hosted Supabase, so the **official `@supabase/supabase-js` SDK works unchanged** - REST, Auth, Storage, and Realtime.
|
|
6
|
+
|
|
7
|
+
One command, one small binary, real Postgres with Row Level Security - and 1:1 with Supabase's APIs and migration conventions.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npx tinbase start
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- **No Docker, no external services.** One runtime dependency: `@electric-sql/pglite`.
|
|
14
|
+
- **Real Postgres semantics.** RLS policies, `auth.uid()`, triggers, FKs - it is Postgres.
|
|
15
|
+
- **Two engines, one API.** Default is PGlite (WASM Postgres - portable, browser-ready). `--engine native` runs an embedded native Postgres instead: ~53 MB of RAM at boot, PocketBase-class footprint, zero semantic differences.
|
|
16
|
+
- **Supabase CLI migration conventions.** Reads `supabase/migrations/*.sql` and `supabase/seed.sql`; tracks them in `supabase_migrations.schema_migrations`. Your migration files stay portable to hosted Supabase.
|
|
17
|
+
- **Browser-ready core.** Every service is a pure `(Request) => Response` fetch handler. In Node it's served over HTTP; in the browser you can hand it to supabase-js as a custom `fetch` and run the whole backend in-process (PGlite already runs in the browser via IndexedDB/OPFS).
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# in a project with a supabase/ directory (or none - it still boots)
|
|
23
|
+
npx tinbase start
|
|
24
|
+
|
|
25
|
+
# API URL: http://127.0.0.1:54321
|
|
26
|
+
# anon key: eyJ...
|
|
27
|
+
# service_role key: eyJ...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Point the ordinary supabase-js client at it:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { createClient } from '@supabase/supabase-js'
|
|
34
|
+
|
|
35
|
+
const supabase = createClient('http://127.0.0.1:54321', ANON_KEY)
|
|
36
|
+
|
|
37
|
+
await supabase.auth.signUp({ email: 'me@example.com', password: 'secret123' })
|
|
38
|
+
await supabase.from('todos').insert({ title: 'hello' })
|
|
39
|
+
const { data } = await supabase.from('todos').select('*, author:users(name)').eq('done', false)
|
|
40
|
+
|
|
41
|
+
supabase
|
|
42
|
+
.channel('feed')
|
|
43
|
+
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'todos' }, console.log)
|
|
44
|
+
.subscribe()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### CLI
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
tinbase start # boot the server (applies pending migrations first)
|
|
51
|
+
tinbase migrate # apply pending migrations and exit
|
|
52
|
+
tinbase status # list applied migrations
|
|
53
|
+
tinbase keys # print anon / service_role keys
|
|
54
|
+
|
|
55
|
+
-p, --port <n> port (default 54321)
|
|
56
|
+
--dir <path> project dir containing supabase/ (default cwd)
|
|
57
|
+
--data-dir <path> PGlite data dir (default <dir>/.tinbase/db)
|
|
58
|
+
--jwt-secret <s> JWT secret (or TINBASE_JWT_SECRET)
|
|
59
|
+
--memory in-memory database, no persistence (wasm engine)
|
|
60
|
+
--engine <e> wasm (default) or native
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Engines
|
|
64
|
+
|
|
65
|
+
- **wasm** (default): PGlite. Zero setup, runs anywhere Node runs - and in the browser. Costs ~350 MB RAM.
|
|
66
|
+
- **native**: embedded native Postgres 17. First run downloads platform binaries (~12 MB, from [theseus-rs/postgresql-binaries](https://github.com/theseus-rs/postgresql-binaries), cached in `~/.cache/tinbase`), then `initdb` with memory-lean settings. ~53 MB RAM at boot. Listens only on a private unix socket (0700 dir, trust auth) - never TCP. macOS/Linux on x64/arm64; on Windows use wasm.
|
|
67
|
+
|
|
68
|
+
Both engines run the identical bootstrap, migrations, RLS, and realtime CDC - the test suite passes on both (`TINBASE_TEST_ENGINE=native npm test`).
|
|
69
|
+
|
|
70
|
+
### Single-binary build (PocketBase-style)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm run build:binary # requires bun; emits dist-bin/tinbase (~57 MB)
|
|
74
|
+
|
|
75
|
+
./tinbase start # that's the whole deployment
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
One compiled executable, no Node or npm on the target machine. It defaults to the native engine (Postgres binaries auto-download on first run, 12 MB) and serves everything - REST, Auth, Storage, Realtime WebSockets - at 44 MB of RAM at boot. Runs under Bun's runtime via a Bun-native server (`Bun.serve` + built-in WebSockets); the same CLI on Node uses the node:http server.
|
|
79
|
+
|
|
80
|
+
## Embedding (Node or browser)
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { createBackend } from 'tinbase'
|
|
84
|
+
|
|
85
|
+
const backend = await createBackend({
|
|
86
|
+
// dataDir: 'idb://my-app' <- browser persistence
|
|
87
|
+
migrations: [{ name: '20240101000000_init', sql: 'create table notes (...)' }],
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// supabase-js talks to it in-process - no HTTP server, no network
|
|
91
|
+
const supabase = createClient('http://localhost', backend.anonKey, {
|
|
92
|
+
global: { fetch: (input, init) => backend.fetch(new Request(input, init)) },
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Node-only helpers live in `tinbase/node`:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { serve, FsStorageDriver, loadSupabaseProject } from 'tinbase/node'
|
|
100
|
+
|
|
101
|
+
const project = await loadSupabaseProject(process.cwd())
|
|
102
|
+
const backend = await createBackend({ ...project, storageDriver: new FsStorageDriver('./files') })
|
|
103
|
+
const server = await serve(backend, { port: 54321 })
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## What's implemented
|
|
107
|
+
|
|
108
|
+
| Service | Endpoint | Coverage |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| REST (PostgREST) | `/rest/v1` | select with embedded resources (to-one, to-many, many-to-many via junction, nested, `!inner`, aliases, hints, casts, JSON paths), all common filter operators incl. `or`/`and` trees, full-text search, order/limit/offset (top-level and per-embed), `single`/`maybeSingle`, `count`, insert/bulk insert, upsert (merge/ignore), update, delete, `Prefer` handling, RPC (scalar, `setof`, void, filters on results), PostgREST-shaped errors |
|
|
111
|
+
| Auth (GoTrue) | `/auth/v1` | email/password signup + sign-in, anonymous sign-in, session refresh with rotation, `getUser`, `updateUser`, sign-out, admin user CRUD (service key), GoTrue-shaped errors. JWTs are HS256 via WebCrypto; passwords are PBKDF2 |
|
|
112
|
+
| Storage | `/storage/v1` | bucket CRUD, upload (raw + multipart), download, public objects, signed URLs, signed upload URLs, list with folder entries, move/copy, remove, size/MIME limits. Metadata lives in `storage.objects` with RLS enforced; bytes go through a pluggable driver (fs in Node, memory anywhere) |
|
|
113
|
+
| Realtime | `/realtime/v1` | Phoenix protocol (v1 JSON and v2 array/binary serializers), `postgres_changes` (INSERT/UPDATE/DELETE, filters) fed by triggers + `pg_notify`, broadcast (incl. binary payloads), presence. WebSocket server is a ~150-line RFC 6455 implementation - no `ws` dependency |
|
|
114
|
+
|
|
115
|
+
### RLS works like real Supabase
|
|
116
|
+
|
|
117
|
+
Every REST/storage request runs inside a transaction with `SET LOCAL role` and `request.jwt.claims`, so policies like this behave identically to hosted Supabase:
|
|
118
|
+
|
|
119
|
+
```sql
|
|
120
|
+
create policy "own rows" on todos
|
|
121
|
+
for all to authenticated
|
|
122
|
+
using (user_id = auth.uid()) with check (user_id = auth.uid());
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Footprint: tinbase vs PocketBase vs Supabase local
|
|
126
|
+
|
|
127
|
+

|
|
128
|
+
|
|
129
|
+
Measured on an Apple Silicon Mac (48 GB), macOS 15. Same workload for all three: boot with one migrated table, then 1,000 single-row inserts followed by 1,000 filtered list queries. Memory is physical footprint (`vmmap`) for native processes and the sum of `docker stats` for containers. Reproduce with [`bench/footprint.ts`](bench/footprint.ts); raw numbers in [`bench/results.json`](bench/results.json).
|
|
130
|
+
|
|
131
|
+
| | tinbase (single binary) | tinbase (native, Node) | tinbase (wasm) | PocketBase v0.39.5 | Supabase local (CLI 2.40) |
|
|
132
|
+
| --- | --- | --- | --- | --- | --- |
|
|
133
|
+
| Database | real Postgres 17 + RLS | real Postgres 17 + RLS | real Postgres (PGlite) + RLS | SQLite | Postgres 17 |
|
|
134
|
+
| Runtime memory at boot | 44 MB | 53 MB | 573 MB | 16 MB | 1,441 MB |
|
|
135
|
+
| Runtime memory after workload | 64 MB | 96 MB | 347 MB¹ | 25 MB | 1,626 MB |
|
|
136
|
+
| Data on disk (1k rows) | 38 MB | 38 MB | 39 MB | 7 MB | 70 MB |
|
|
137
|
+
| Install size | 92 MB (no runtime needed) | 35 MB² | 26 MB² | 30 MB | 2,291 MB³ |
|
|
138
|
+
| Processes | 2 (tinbase + postgres) | 2 (node + postgres) | 1 | 1 | 12 containers + Docker |
|
|
139
|
+
| 1,000 inserts | 0.4 s | 0.5 s | 0.8 s | 0.3 s | 1.1 s |
|
|
140
|
+
| 1,000 filtered reads | 0.4 s | 0.4 s | 0.8 s | 0.3 s | 1.0 s |
|
|
141
|
+
|
|
142
|
+
¹ PGlite's WASM instantiation peaks at boot, then the OS reclaims pages; steady state under load is ~350 MB.
|
|
143
|
+
² Native: unpacked Postgres 17 binaries + `dist`. Wasm: `dist` + `@electric-sql/pglite`. Both exclude the Node runtime you already have.
|
|
144
|
+
³ Sum of the Docker image sizes the default local stack runs, excluding Docker Desktop itself.
|
|
145
|
+
|
|
146
|
+
**How to read this honestly:**
|
|
147
|
+
|
|
148
|
+
- **vs Supabase local**: same SDK, same APIs, ~15-27x less memory (native engine), ~65x smaller install, 2 processes instead of a 12-container stack, and boots in ~2 s instead of a minute. That's the entire point of the project.
|
|
149
|
+
- **vs PocketBase**: the single binary lands in PocketBase's weight class - ~2.5x the RAM, one downloadable file, no runtime prerequisite - while running *real Postgres* (RLS, jsonb, FKs, triggers) behind Supabase's exact wire APIs, so your code and migration files move to hosted Supabase unchanged. PocketBase is still the lightest option if you don't need any of that.
|
|
150
|
+
- The wasm engine's memory is almost entirely the PGlite WASM heap; the API layers add single-digit MB. Use it where portability matters (browser, one-dependency installs); use `--engine native` on servers.
|
|
151
|
+
|
|
152
|
+
## How complete is it?
|
|
153
|
+
|
|
154
|
+
Rough coverage of the supabase-js SDK surface, measured against what each sub-library can express (all "supported" claims are exercised by the test suite):
|
|
155
|
+
|
|
156
|
+
| Module | Coverage | Supported | Missing |
|
|
157
|
+
| --- | --- | --- | --- |
|
|
158
|
+
| Database (`postgrest-js`) | ~85% | full filter grammar, embeds (to-one/to-many/m2m/nested/`!inner`), JSON paths, upsert, count, single/maybeSingle, RPC | aggregates in select, full spread embeds, `.explain()`, `.csv()`, geojson |
|
|
159
|
+
| Auth (`auth-js`) | ~40% | email/password, anonymous sign-in, refresh rotation, user updates, admin CRUD | OAuth, magic links, OTP, MFA, SSO/SAML, PKCE, phone auth, email delivery |
|
|
160
|
+
| Storage (`storage-js`) | ~80% | buckets, upload/download, signed URLs + signed uploads, list/move/copy/remove, size/MIME limits | resumable (TUS) uploads, image transformations |
|
|
161
|
+
| Realtime (`realtime-js`) | ~70% | postgres_changes with filters, broadcast (incl. binary), presence, v1+v2 serializers | RLS-filtered fan-out (WALRUS), private channel auth, DB-triggered broadcast |
|
|
162
|
+
| Edge Functions (`functions-js`) | 0% | - | everything (`supabase.functions.invoke()`) |
|
|
163
|
+
|
|
164
|
+
**Overall: roughly 65% of the SDK surface - but ~90% of what a typical CRUD + auth + storage + realtime app actually calls.** The biggest real-world gaps are OAuth logins and edge functions.
|
|
165
|
+
|
|
166
|
+
## Known gaps
|
|
167
|
+
|
|
168
|
+
- `postgres_changes` does not apply RLS to fan-out (all subscribers see change events); hosted Supabase filters via WALRUS.
|
|
169
|
+
- Spread embeds (`...rel(col)`) support flat column lists only; aggregate functions in `select` are not implemented.
|
|
170
|
+
- Auth: no OAuth providers, magic links, or OTP (endpoints accept and no-op); no email delivery.
|
|
171
|
+
- `pg_notify` payloads cap at ~8 kB - realtime events for larger rows arrive with `record: null` and an `errors` entry, like Supabase's "payload too large".
|
|
172
|
+
- One writer at a time: PGlite is single-connection, and the native engine currently serializes requests over one connection for parity (a connection pool is a straightforward future upgrade). Fine for dev tools and small apps, not for high-concurrency production.
|
|
173
|
+
|
|
174
|
+
## Tests
|
|
175
|
+
|
|
176
|
+
53 integration tests + 4 realtime e2e tests run the real `@supabase/supabase-js` against the backend (REST via in-process fetch, realtime over actual WebSockets):
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
npm test
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Why
|
|
183
|
+
|
|
184
|
+
1. A lighter Supabase for local dev and self-contained apps - `npx tinbase start` instead of Docker Compose.
|
|
185
|
+
2. The whole backend can run **in the browser** (PGlite WASM + fetch-handler architecture), e.g. inside [lifo.sh](https://lifo.sh).
|
package/assets/logo.svg
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="tin" x1="24" y1="30" x2="96" y2="104" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#10b981"/>
|
|
5
|
+
<stop offset="1" stop-color="#047857"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<!-- can body: the classic database cylinder, tin-sized -->
|
|
9
|
+
<path d="M24 34v56c0 8.8 16.1 16 36 16s36-7.2 36-16V34" fill="url(#tin)"/>
|
|
10
|
+
<!-- seam highlights -->
|
|
11
|
+
<path d="M24 62c0 8.8 16.1 16 36 16s36-7.2 36-16" stroke="#ffffff" stroke-opacity="0.35" stroke-width="4" fill="none"/>
|
|
12
|
+
<!-- lid -->
|
|
13
|
+
<ellipse cx="60" cy="34" rx="36" ry="16" fill="#34d399"/>
|
|
14
|
+
<ellipse cx="60" cy="34" rx="36" ry="16" stroke="#065f46" stroke-opacity="0.25" stroke-width="2"/>
|
|
15
|
+
<!-- pull tab -->
|
|
16
|
+
<circle cx="72" cy="31" r="7" fill="none" stroke="#065f46" stroke-width="4"/>
|
|
17
|
+
<rect x="54" y="27.5" width="14" height="5" rx="2.5" fill="#065f46" transform="rotate(8 54 30)"/>
|
|
18
|
+
</svg>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GoTrue-compatible auth endpoints (/auth/v1/*) — the subset supabase-js
|
|
3
|
+
* uses for email/password auth, sessions, and admin user management.
|
|
4
|
+
*/
|
|
5
|
+
import type { Database } from '../db/database.js';
|
|
6
|
+
import type { RequestContext } from '../types.js';
|
|
7
|
+
export interface AuthConfig {
|
|
8
|
+
jwtSecret: string;
|
|
9
|
+
siteUrl: string;
|
|
10
|
+
jwtExpiry: number;
|
|
11
|
+
}
|
|
12
|
+
interface UserRow {
|
|
13
|
+
id: string;
|
|
14
|
+
aud: string | null;
|
|
15
|
+
role: string | null;
|
|
16
|
+
email: string | null;
|
|
17
|
+
encrypted_password: string | null;
|
|
18
|
+
email_confirmed_at: Date | string | null;
|
|
19
|
+
last_sign_in_at: Date | string | null;
|
|
20
|
+
raw_app_meta_data: Record<string, unknown> | null;
|
|
21
|
+
raw_user_meta_data: Record<string, unknown> | null;
|
|
22
|
+
created_at: Date | string | null;
|
|
23
|
+
updated_at: Date | string | null;
|
|
24
|
+
phone: string | null;
|
|
25
|
+
phone_confirmed_at: Date | string | null;
|
|
26
|
+
is_anonymous: boolean | null;
|
|
27
|
+
}
|
|
28
|
+
export declare class AuthHandler {
|
|
29
|
+
private db;
|
|
30
|
+
private config;
|
|
31
|
+
constructor(db: Database, config: AuthConfig);
|
|
32
|
+
handle(req: Request, ctx: RequestContext, url: URL): Promise<Response>;
|
|
33
|
+
private signup;
|
|
34
|
+
private token;
|
|
35
|
+
private getUser;
|
|
36
|
+
private updateUser;
|
|
37
|
+
private logout;
|
|
38
|
+
private admin;
|
|
39
|
+
private userFromBearer;
|
|
40
|
+
userJson(u: UserRow): Record<string, unknown>;
|
|
41
|
+
private sessionFor;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { randomToken, signJwt, verifyJwt } from '../jwt.js';
|
|
2
|
+
import { hashPassword, verifyPassword } from './password.js';
|
|
3
|
+
function authError(status, errorCode, msg) {
|
|
4
|
+
return json(status, { code: status, error_code: errorCode, msg });
|
|
5
|
+
}
|
|
6
|
+
function json(status, body) {
|
|
7
|
+
return new Response(status === 204 ? null : JSON.stringify(body), {
|
|
8
|
+
status,
|
|
9
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function iso(v) {
|
|
13
|
+
if (v === null || v === undefined)
|
|
14
|
+
return null;
|
|
15
|
+
return v instanceof Date ? v.toISOString() : new Date(v).toISOString();
|
|
16
|
+
}
|
|
17
|
+
export class AuthHandler {
|
|
18
|
+
db;
|
|
19
|
+
config;
|
|
20
|
+
constructor(db, config) {
|
|
21
|
+
this.db = db;
|
|
22
|
+
this.config = config;
|
|
23
|
+
}
|
|
24
|
+
async handle(req, ctx, url) {
|
|
25
|
+
const path = url.pathname.replace(/^\/auth\/v1\/?/, '').replace(/\/+$/, '');
|
|
26
|
+
const method = req.method.toUpperCase();
|
|
27
|
+
try {
|
|
28
|
+
if (path === 'health')
|
|
29
|
+
return json(200, { name: 'tinbase-auth', version: '0.1.0', description: 'GoTrue-compatible auth' });
|
|
30
|
+
if (path === 'settings') {
|
|
31
|
+
return json(200, {
|
|
32
|
+
external: { email: true, phone: false, anonymous_users: true },
|
|
33
|
+
disable_signup: false,
|
|
34
|
+
autoconfirm: true,
|
|
35
|
+
mailer_autoconfirm: true,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (path === 'signup' && method === 'POST')
|
|
39
|
+
return await this.signup(req);
|
|
40
|
+
if (path === 'token' && method === 'POST')
|
|
41
|
+
return await this.token(req, url);
|
|
42
|
+
if (path === 'user' && method === 'GET')
|
|
43
|
+
return await this.getUser(req);
|
|
44
|
+
if (path === 'user' && method === 'PUT')
|
|
45
|
+
return await this.updateUser(req);
|
|
46
|
+
if (path === 'logout' && method === 'POST')
|
|
47
|
+
return await this.logout(req);
|
|
48
|
+
if (['recover', 'otp', 'magiclink', 'resend'].includes(path) && method === 'POST') {
|
|
49
|
+
// no mail transport in tinbase — accept and no-op so flows don't crash
|
|
50
|
+
return json(200, {});
|
|
51
|
+
}
|
|
52
|
+
if (path.startsWith('admin/users'))
|
|
53
|
+
return await this.admin(req, ctx, path, method);
|
|
54
|
+
return authError(404, 'not_found', `unknown auth endpoint: ${path}`);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
58
|
+
return authError(500, 'unexpected_failure', msg);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// ── flows ─────────────────────────────────────────────────────────────
|
|
62
|
+
async signup(req) {
|
|
63
|
+
const body = (await req.json().catch(() => ({})));
|
|
64
|
+
if (!body.email && !body.password) {
|
|
65
|
+
// supabase.auth.signInAnonymously()
|
|
66
|
+
const res = await this.db.query(`insert into auth.users (aud, role, raw_app_meta_data, raw_user_meta_data, is_anonymous, last_sign_in_at)
|
|
67
|
+
values ('authenticated', 'authenticated', '{}', $1, true, now())
|
|
68
|
+
returning *`, [JSON.stringify(body.data ?? {})]);
|
|
69
|
+
return json(200, await this.sessionFor(res.rows[0]));
|
|
70
|
+
}
|
|
71
|
+
if (!body.email || !body.password) {
|
|
72
|
+
return authError(400, 'validation_failed', 'Signup requires a valid email and password');
|
|
73
|
+
}
|
|
74
|
+
if (body.password.length < 6) {
|
|
75
|
+
return authError(422, 'weak_password', 'Password should be at least 6 characters.');
|
|
76
|
+
}
|
|
77
|
+
const email = body.email.toLowerCase().trim();
|
|
78
|
+
const existing = await this.db.query(`select id from auth.users where email = $1`, [email]);
|
|
79
|
+
if (existing.rows.length > 0) {
|
|
80
|
+
return authError(422, 'user_already_exists', 'User already registered');
|
|
81
|
+
}
|
|
82
|
+
const hashed = await hashPassword(body.password);
|
|
83
|
+
const res = await this.db.query(`insert into auth.users
|
|
84
|
+
(aud, role, email, encrypted_password, email_confirmed_at, last_sign_in_at,
|
|
85
|
+
raw_app_meta_data, raw_user_meta_data)
|
|
86
|
+
values ('authenticated', 'authenticated', $1, $2, now(), now(),
|
|
87
|
+
'{"provider":"email","providers":["email"]}', $3)
|
|
88
|
+
returning *`, [email, hashed, JSON.stringify(body.data ?? {})]);
|
|
89
|
+
return json(200, await this.sessionFor(res.rows[0]));
|
|
90
|
+
}
|
|
91
|
+
async token(req, url) {
|
|
92
|
+
const grantType = url.searchParams.get('grant_type');
|
|
93
|
+
const body = (await req.json().catch(() => ({})));
|
|
94
|
+
if (grantType === 'password') {
|
|
95
|
+
const email = (body.email ?? '').toLowerCase().trim();
|
|
96
|
+
const res = await this.db.query(`select * from auth.users where email = $1`, [email]);
|
|
97
|
+
const user = res.rows[0];
|
|
98
|
+
if (!user || !user.encrypted_password || !(await verifyPassword(body.password ?? '', user.encrypted_password))) {
|
|
99
|
+
return authError(400, 'invalid_credentials', 'Invalid login credentials');
|
|
100
|
+
}
|
|
101
|
+
await this.db.query(`update auth.users set last_sign_in_at = now() where id = $1`, [user.id]);
|
|
102
|
+
return json(200, await this.sessionFor(user));
|
|
103
|
+
}
|
|
104
|
+
if (grantType === 'refresh_token') {
|
|
105
|
+
const token = body.refresh_token;
|
|
106
|
+
if (!token)
|
|
107
|
+
return authError(400, 'validation_failed', 'refresh_token required');
|
|
108
|
+
const res = await this.db.query(`select rt.*, u.id as uid from auth.refresh_tokens rt
|
|
109
|
+
join auth.users u on u.id = rt.user_id
|
|
110
|
+
where rt.token = $1`, [token]);
|
|
111
|
+
const row = res.rows[0];
|
|
112
|
+
if (!row || row.revoked) {
|
|
113
|
+
return authError(400, 'refresh_token_not_found', 'Invalid Refresh Token: Refresh Token Not Found');
|
|
114
|
+
}
|
|
115
|
+
await this.db.query(`update auth.refresh_tokens set revoked = true, updated_at = now() where token = $1`, [token]);
|
|
116
|
+
const ures = await this.db.query(`select * from auth.users where id = $1`, [row.user_id]);
|
|
117
|
+
return json(200, await this.sessionFor(ures.rows[0], token));
|
|
118
|
+
}
|
|
119
|
+
return authError(400, 'invalid_grant', `unsupported grant_type: ${grantType}`);
|
|
120
|
+
}
|
|
121
|
+
async getUser(req) {
|
|
122
|
+
const user = await this.userFromBearer(req);
|
|
123
|
+
if (!user)
|
|
124
|
+
return authError(401, 'no_authorization', 'Invalid or expired token');
|
|
125
|
+
return json(200, this.userJson(user));
|
|
126
|
+
}
|
|
127
|
+
async updateUser(req) {
|
|
128
|
+
const user = await this.userFromBearer(req);
|
|
129
|
+
if (!user)
|
|
130
|
+
return authError(401, 'no_authorization', 'Invalid or expired token');
|
|
131
|
+
const body = (await req.json().catch(() => ({})));
|
|
132
|
+
const sets = [];
|
|
133
|
+
const params = [];
|
|
134
|
+
if (body.email) {
|
|
135
|
+
params.push(body.email.toLowerCase().trim());
|
|
136
|
+
sets.push(`email = $${params.length}, email_confirmed_at = now()`);
|
|
137
|
+
}
|
|
138
|
+
if (body.password) {
|
|
139
|
+
if (body.password.length < 6) {
|
|
140
|
+
return authError(422, 'weak_password', 'Password should be at least 6 characters.');
|
|
141
|
+
}
|
|
142
|
+
params.push(await hashPassword(body.password));
|
|
143
|
+
sets.push(`encrypted_password = $${params.length}`);
|
|
144
|
+
}
|
|
145
|
+
if (body.data) {
|
|
146
|
+
params.push(JSON.stringify(body.data));
|
|
147
|
+
sets.push(`raw_user_meta_data = coalesce(raw_user_meta_data, '{}'::jsonb) || $${params.length}::jsonb`);
|
|
148
|
+
}
|
|
149
|
+
if (sets.length === 0)
|
|
150
|
+
return json(200, this.userJson(user));
|
|
151
|
+
params.push(user.id);
|
|
152
|
+
const res = await this.db.query(`update auth.users set ${sets.join(', ')}, updated_at = now() where id = $${params.length} returning *`, params);
|
|
153
|
+
return json(200, this.userJson(res.rows[0]));
|
|
154
|
+
}
|
|
155
|
+
async logout(req) {
|
|
156
|
+
const user = await this.userFromBearer(req);
|
|
157
|
+
if (user) {
|
|
158
|
+
await this.db.query(`update auth.refresh_tokens set revoked = true where user_id = $1`, [user.id]);
|
|
159
|
+
}
|
|
160
|
+
return new Response(null, { status: 204 });
|
|
161
|
+
}
|
|
162
|
+
// ── admin ─────────────────────────────────────────────────────────────
|
|
163
|
+
async admin(req, ctx, path, method) {
|
|
164
|
+
if (ctx.role !== 'service_role') {
|
|
165
|
+
return authError(403, 'insufficient_permissions', 'Admin endpoints require the service_role key');
|
|
166
|
+
}
|
|
167
|
+
const idMatch = path.match(/^admin\/users\/([0-9a-f-]{36})$/);
|
|
168
|
+
if (path === 'admin/users' && method === 'GET') {
|
|
169
|
+
const res = await this.db.query(`select * from auth.users order by created_at desc limit 1000`);
|
|
170
|
+
return json(200, { users: res.rows.map((u) => this.userJson(u)), aud: 'authenticated' });
|
|
171
|
+
}
|
|
172
|
+
if (path === 'admin/users' && method === 'POST') {
|
|
173
|
+
const body = (await req.json().catch(() => ({})));
|
|
174
|
+
if (!body.email)
|
|
175
|
+
return authError(400, 'validation_failed', 'email is required');
|
|
176
|
+
const hashed = body.password ? await hashPassword(body.password) : null;
|
|
177
|
+
const res = await this.db.query(`insert into auth.users
|
|
178
|
+
(aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data)
|
|
179
|
+
values ('authenticated', 'authenticated', $1, $2, case when $3 then now() else null end, $4, $5)
|
|
180
|
+
returning *`, [
|
|
181
|
+
body.email.toLowerCase().trim(),
|
|
182
|
+
hashed,
|
|
183
|
+
body.email_confirm ?? true,
|
|
184
|
+
JSON.stringify({ provider: 'email', providers: ['email'], ...(body.app_metadata ?? {}) }),
|
|
185
|
+
JSON.stringify(body.user_metadata ?? {}),
|
|
186
|
+
]);
|
|
187
|
+
return json(200, this.userJson(res.rows[0]));
|
|
188
|
+
}
|
|
189
|
+
if (idMatch && method === 'GET') {
|
|
190
|
+
const res = await this.db.query(`select * from auth.users where id = $1`, [idMatch[1]]);
|
|
191
|
+
if (res.rows.length === 0)
|
|
192
|
+
return authError(404, 'user_not_found', 'User not found');
|
|
193
|
+
return json(200, this.userJson(res.rows[0]));
|
|
194
|
+
}
|
|
195
|
+
if (idMatch && method === 'PUT') {
|
|
196
|
+
const body = (await req.json().catch(() => ({})));
|
|
197
|
+
const sets = [];
|
|
198
|
+
const params = [];
|
|
199
|
+
if (typeof body.email === 'string') {
|
|
200
|
+
params.push(body.email.toLowerCase().trim());
|
|
201
|
+
sets.push(`email = $${params.length}`);
|
|
202
|
+
}
|
|
203
|
+
if (typeof body.password === 'string') {
|
|
204
|
+
params.push(await hashPassword(body.password));
|
|
205
|
+
sets.push(`encrypted_password = $${params.length}`);
|
|
206
|
+
}
|
|
207
|
+
if (body.user_metadata) {
|
|
208
|
+
params.push(JSON.stringify(body.user_metadata));
|
|
209
|
+
sets.push(`raw_user_meta_data = $${params.length}::jsonb`);
|
|
210
|
+
}
|
|
211
|
+
if (body.app_metadata) {
|
|
212
|
+
params.push(JSON.stringify(body.app_metadata));
|
|
213
|
+
sets.push(`raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb) || $${params.length}::jsonb`);
|
|
214
|
+
}
|
|
215
|
+
if (body.email_confirm === true)
|
|
216
|
+
sets.push(`email_confirmed_at = now()`);
|
|
217
|
+
if (sets.length === 0)
|
|
218
|
+
return authError(400, 'validation_failed', 'nothing to update');
|
|
219
|
+
params.push(idMatch[1]);
|
|
220
|
+
const res = await this.db.query(`update auth.users set ${sets.join(', ')}, updated_at = now() where id = $${params.length} returning *`, params);
|
|
221
|
+
if (res.rows.length === 0)
|
|
222
|
+
return authError(404, 'user_not_found', 'User not found');
|
|
223
|
+
return json(200, this.userJson(res.rows[0]));
|
|
224
|
+
}
|
|
225
|
+
if (idMatch && method === 'DELETE') {
|
|
226
|
+
await this.db.query(`delete from auth.users where id = $1`, [idMatch[1]]);
|
|
227
|
+
return json(200, {});
|
|
228
|
+
}
|
|
229
|
+
return authError(404, 'not_found', `unknown admin endpoint`);
|
|
230
|
+
}
|
|
231
|
+
// ── helpers ───────────────────────────────────────────────────────────
|
|
232
|
+
async userFromBearer(req) {
|
|
233
|
+
const authz = req.headers.get('authorization') ?? '';
|
|
234
|
+
if (!authz.toLowerCase().startsWith('bearer '))
|
|
235
|
+
return null;
|
|
236
|
+
const claims = await verifyJwt(authz.slice(7), this.config.jwtSecret);
|
|
237
|
+
if (!claims?.sub)
|
|
238
|
+
return null;
|
|
239
|
+
const res = await this.db.query(`select * from auth.users where id = $1`, [claims.sub]);
|
|
240
|
+
return res.rows[0] ?? null;
|
|
241
|
+
}
|
|
242
|
+
userJson(u) {
|
|
243
|
+
return {
|
|
244
|
+
id: u.id,
|
|
245
|
+
aud: u.aud ?? 'authenticated',
|
|
246
|
+
role: u.role ?? 'authenticated',
|
|
247
|
+
email: u.email ?? '',
|
|
248
|
+
email_confirmed_at: iso(u.email_confirmed_at),
|
|
249
|
+
phone: u.phone ?? '',
|
|
250
|
+
confirmed_at: iso(u.email_confirmed_at),
|
|
251
|
+
last_sign_in_at: iso(u.last_sign_in_at),
|
|
252
|
+
app_metadata: u.raw_app_meta_data ?? {},
|
|
253
|
+
user_metadata: u.raw_user_meta_data ?? {},
|
|
254
|
+
identities: [],
|
|
255
|
+
created_at: iso(u.created_at),
|
|
256
|
+
updated_at: iso(u.updated_at),
|
|
257
|
+
is_anonymous: u.is_anonymous ?? false,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
async sessionFor(user, parentToken) {
|
|
261
|
+
const now = Math.floor(Date.now() / 1000);
|
|
262
|
+
const expiresAt = now + this.config.jwtExpiry;
|
|
263
|
+
const sessionId = crypto.randomUUID();
|
|
264
|
+
const claims = {
|
|
265
|
+
iss: `${this.config.siteUrl}/auth/v1`,
|
|
266
|
+
sub: user.id,
|
|
267
|
+
aud: user.aud ?? 'authenticated',
|
|
268
|
+
exp: expiresAt,
|
|
269
|
+
iat: now,
|
|
270
|
+
email: user.email ?? '',
|
|
271
|
+
phone: user.phone ?? '',
|
|
272
|
+
app_metadata: user.raw_app_meta_data ?? {},
|
|
273
|
+
user_metadata: user.raw_user_meta_data ?? {},
|
|
274
|
+
role: user.role ?? 'authenticated',
|
|
275
|
+
is_anonymous: user.is_anonymous ?? false,
|
|
276
|
+
session_id: sessionId,
|
|
277
|
+
};
|
|
278
|
+
const accessToken = await signJwt(claims, this.config.jwtSecret);
|
|
279
|
+
const refreshToken = randomToken(24);
|
|
280
|
+
await this.db.query(`insert into auth.refresh_tokens (token, user_id, parent, session_id) values ($1, $2, $3, $4)`, [refreshToken, user.id, parentToken ?? null, sessionId]);
|
|
281
|
+
return {
|
|
282
|
+
access_token: accessToken,
|
|
283
|
+
token_type: 'bearer',
|
|
284
|
+
expires_in: this.config.jwtExpiry,
|
|
285
|
+
expires_at: expiresAt,
|
|
286
|
+
refresh_token: refreshToken,
|
|
287
|
+
user: this.userJson(user),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/auth/handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** PBKDF2-SHA256 password hashing on WebCrypto (portable, no native deps). */
|
|
2
|
+
const ITERATIONS = 100_000;
|
|
3
|
+
function toHex(bytes) {
|
|
4
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
5
|
+
}
|
|
6
|
+
function fromHex(hex) {
|
|
7
|
+
const out = new Uint8Array(hex.length / 2);
|
|
8
|
+
for (let i = 0; i < out.length; i++)
|
|
9
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
async function derive(password, salt, iterations) {
|
|
13
|
+
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(password), 'PBKDF2', false, [
|
|
14
|
+
'deriveBits',
|
|
15
|
+
]);
|
|
16
|
+
const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-256', salt: salt, iterations }, key, 256);
|
|
17
|
+
return new Uint8Array(bits);
|
|
18
|
+
}
|
|
19
|
+
export async function hashPassword(password) {
|
|
20
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
21
|
+
const hash = await derive(password, salt, ITERATIONS);
|
|
22
|
+
return `pbkdf2$${ITERATIONS}$${toHex(salt)}$${toHex(hash)}`;
|
|
23
|
+
}
|
|
24
|
+
export async function verifyPassword(password, stored) {
|
|
25
|
+
const parts = stored.split('$');
|
|
26
|
+
if (parts.length !== 4 || parts[0] !== 'pbkdf2')
|
|
27
|
+
return false;
|
|
28
|
+
const iterations = parseInt(parts[1], 10);
|
|
29
|
+
const salt = fromHex(parts[2]);
|
|
30
|
+
const expected = parts[3];
|
|
31
|
+
const hash = await derive(password, salt, iterations);
|
|
32
|
+
const actual = toHex(hash);
|
|
33
|
+
if (actual.length !== expected.length)
|
|
34
|
+
return false;
|
|
35
|
+
let diff = 0;
|
|
36
|
+
for (let i = 0; i < actual.length; i++)
|
|
37
|
+
diff |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
38
|
+
return diff === 0;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=password.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/auth/password.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAE9E,MAAM,UAAU,GAAG,OAAO,CAAA;AAE1B,SAAS,KAAK,CAAC,KAAiB;IAC9B,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACxE,CAAC;AAED,SAAS,OAAO,CAAC,GAAW;IAC1B,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;IAC1C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IACvF,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,QAAgB,EAAE,IAAgB,EAAE,UAAkB;IAC1E,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE;QACpG,YAAY;KACb,CAAC,CAAA;IACF,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,UAAU,CACzC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAoB,EAAE,UAAU,EAAE,EAC3E,GAAG,EACH,GAAG,CACJ,CAAA;IACD,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAA;AAC7B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,MAAM,IAAI,GAAG,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAA;IACvD,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,UAAU,CAAC,CAAA;IACrD,OAAO,UAAU,UAAU,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAA;AAC7D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,QAAgB,EAAE,MAAc;IACnE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC7D,MAAM,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IACzC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAC9B,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACzB,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,QAAQ,EAAE,IAAI,EAAE,UAAU,CAAC,CAAA;IACrD,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAA;IAC1B,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACnD,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;IAC7F,OAAO,IAAI,KAAK,CAAC,CAAA;AACnB,CAAC"}
|
package/dist/cli.d.ts
ADDED