@avelonjs/neon 0.3.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 +351 -0
- package/package.json +56 -0
- package/src/database/driver.ts +263 -0
- package/src/database/index.ts +8 -0
- package/src/identity/cookies.ts +54 -0
- package/src/identity/driver.ts +462 -0
- package/src/identity/errors.ts +206 -0
- package/src/identity/index.ts +15 -0
- package/src/identity/local-auth.ts +304 -0
- package/src/identity/types.ts +19 -0
- package/src/index.ts +6 -0
- package/src/queue/driver.ts +271 -0
- package/src/queue/index.ts +1 -0
- package/src/social/driver.ts +118 -0
- package/src/social/index.ts +8 -0
- package/src/social/local-auth.ts +55 -0
- package/src/social/types.ts +7 -0
- package/src/storage/driver.ts +182 -0
- package/src/storage/index.ts +7 -0
- package/src/storage/local-s3.ts +128 -0
- package/src/tokens/driver.ts +220 -0
- package/src/tokens/index.ts +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Yannelli
|
|
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,351 @@
|
|
|
1
|
+
# @avelonjs/neon
|
|
2
|
+
|
|
3
|
+
`@avelonjs/neon` is the Neon stack for Avelon: a transaction-capable Postgres database, Neon Auth identity and social login, S3 object storage, and Postgres-backed Signets and queues. Reach for this package when you want the same six contracts `@avelonjs/supabase` covers, without PostgREST or GoTrue.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @avelonjs/neon
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
export NEON_DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require
|
|
13
|
+
export NEON_AUTH_URL=https://ep-xxx.neonauth.net
|
|
14
|
+
export NEON_S3_BUCKET=avelon-uploads
|
|
15
|
+
export AWS_REGION=us-east-1
|
|
16
|
+
export AWS_ACCESS_KEY_ID=AKIA...
|
|
17
|
+
export AWS_SECRET_ACCESS_KEY=...
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Wire drivers only in `avelon.config.ts`. Application folders never import `@neondatabase/serverless` or `@aws-sdk/client-s3`.
|
|
21
|
+
|
|
22
|
+
## Basic Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createNeonDatabase } from '@avelonjs/neon'
|
|
26
|
+
import type { QueryIR } from '@avelonjs/core'
|
|
27
|
+
|
|
28
|
+
const db = createNeonDatabase({
|
|
29
|
+
url: process.env.NEON_DATABASE_URL,
|
|
30
|
+
instance: 'primary',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const latest: QueryIR = {
|
|
34
|
+
table: 'posts',
|
|
35
|
+
mode: 'select',
|
|
36
|
+
select: ['id', 'title'],
|
|
37
|
+
where: [{ kind: 'null', column: 'published_at', negated: true }],
|
|
38
|
+
relations: [],
|
|
39
|
+
order: [{ column: 'published_at', direction: 'desc' }],
|
|
40
|
+
limit: 10,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await db.execute(latest)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Capabilities
|
|
47
|
+
|
|
48
|
+
| Surface | Notable capabilities |
|
|
49
|
+
| -------- | --------------------------------------------------------------------------------- |
|
|
50
|
+
| database | `transactions: true`, `upsert: true`, `returning: true`, `maxRelationDepth: 8` |
|
|
51
|
+
| identity | `passwords: true`, `magicLinks: true`, `mfa: ['totp']`, `emailVerification: true` |
|
|
52
|
+
| social | providers `github`, `google` |
|
|
53
|
+
| tokens | `abilities: true`, `expiration: true` |
|
|
54
|
+
| storage | `signedUrls: true`, transforms empty |
|
|
55
|
+
| queue | `delayed: true`, `retries: true`, `deadLetter: true` |
|
|
56
|
+
|
|
57
|
+
`rowSecurity` stays `false`. Ward predicates still inject into Query IR. Neon is Postgres, so you can compile RLS yourself through `raw()`; this package does not ship a PostgREST-shaped `syncWards()` surface.
|
|
58
|
+
|
|
59
|
+
## Database
|
|
60
|
+
|
|
61
|
+
Query IR, migrations, and schema checks compile through `@avelonjs/postgres/sql`, the subpath of that package whose import graph carries no `bun` specifier, so `@avelonjs/neon/database` runs on Node as well as Bun. `raw()` returns the `@neondatabase/serverless` query function.
|
|
62
|
+
|
|
63
|
+
Reads, writes, `rpc()`, and migrations go over Neon's HTTP endpoint, which holds no connection between calls. `transaction()` is the exception: the callback reads rows before deciding what to issue next, and Neon's HTTP transactions are non-interactive, so the first call opens a WebSocket `Pool` connection and `close()` ends it. That path needs a global `WebSocket`, which Node has from 22 on; on older runtimes set `neonConfig.webSocketConstructor` before the first transaction.
|
|
64
|
+
|
|
65
|
+
Import from `@avelonjs/neon/database` rather than the package root when the runtime is Node. The root barrel also exports the queue and token drivers, which are built on Bun's SQL client.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { createNeonDatabase } from '@avelonjs/neon'
|
|
69
|
+
|
|
70
|
+
const db = createNeonDatabase({
|
|
71
|
+
migrations: [
|
|
72
|
+
{
|
|
73
|
+
id: '20260828_create_posts',
|
|
74
|
+
up: ['CREATE TABLE posts (id text PRIMARY KEY, title text NOT NULL)'],
|
|
75
|
+
down: ['DROP TABLE posts'],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
await db.plan()
|
|
81
|
+
await db.apply()
|
|
82
|
+
await db.transaction(async (tx) => {
|
|
83
|
+
await tx.execute({
|
|
84
|
+
table: 'posts',
|
|
85
|
+
mode: 'insert',
|
|
86
|
+
select: [],
|
|
87
|
+
where: [],
|
|
88
|
+
relations: [],
|
|
89
|
+
order: [],
|
|
90
|
+
values: { id: 'post-1', title: 'Hello' },
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Identity
|
|
96
|
+
|
|
97
|
+
Neon Auth speaks the Better Auth HTTP surface. The config-time factory receives request-scoped cookies.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { createNeonIdentity } from '@avelonjs/neon'
|
|
101
|
+
|
|
102
|
+
const auth = createNeonIdentity({
|
|
103
|
+
authUrl: process.env.NEON_AUTH_URL,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
export async function currentUser(cookies: import('@avelonjs/core').RequestCookies) {
|
|
107
|
+
return auth(cookies).user()
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Capabilities: `passwords: true`, `magicLinks: true`, `oauth: false`, `organizations: false`, `mfa: ['totp']`, `emailVerification: true`. After password sign-in you challenge the enrolled TOTP factor and verify the authenticator code. `challengeMfa()` without an argument uses `totp`. Email confirmation uses Better Auth's send-and-verify token pair.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { createNeonIdentity } from '@avelonjs/neon'
|
|
115
|
+
import type { RequestCookies } from '@avelonjs/core'
|
|
116
|
+
|
|
117
|
+
export async function completeTotpAndEmail(cookies: RequestCookies) {
|
|
118
|
+
const identity = createNeonIdentity({
|
|
119
|
+
authUrl: process.env.NEON_AUTH_URL,
|
|
120
|
+
})(cookies)
|
|
121
|
+
|
|
122
|
+
await identity.signInWithPassword('jordan@example.com', 'correct-horse-battery')
|
|
123
|
+
const challenge = await identity.challengeMfa('totp')
|
|
124
|
+
await identity.verifyMfa(challenge.id, '123456')
|
|
125
|
+
await identity.sendEmailVerification()
|
|
126
|
+
await identity.verifyEmail('confirmation-token')
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
OAuth linking stays undeclared until a live Neon Auth project exercises that surface in CI.
|
|
131
|
+
|
|
132
|
+
## Social
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { createNeonSocial } from '@avelonjs/neon'
|
|
136
|
+
|
|
137
|
+
const social = createNeonSocial({
|
|
138
|
+
authUrl: process.env.NEON_AUTH_URL,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
export async function githubRedirect(callbackUrl: string): Promise<string> {
|
|
142
|
+
return social.redirect('github', callbackUrl)
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Providers are the literal list `github` and `google`. Undeclared providers raise `Invalid`. A mismatched or missing OAuth `state` raises `Unauthenticated`.
|
|
147
|
+
|
|
148
|
+
## Tokens
|
|
149
|
+
|
|
150
|
+
Neon Auth does not issue named API tokens. Hashed Signets live in Postgres on the Neon database.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { createNeonTokens } from '@avelonjs/neon'
|
|
154
|
+
|
|
155
|
+
const tokens = createNeonTokens({
|
|
156
|
+
url: process.env.NEON_DATABASE_URL,
|
|
157
|
+
subject: 'user-1',
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const issued = await tokens.issue('deployment', { abilities: ['records:read'] })
|
|
161
|
+
await tokens.verify(issued.plainText)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Storage
|
|
165
|
+
|
|
166
|
+
Object bytes persist in S3. Point `endpoint` at AWS, Cloudflare R2, MinIO, or the in-process `LocalS3Server` used by conformance. Signed reads are SigV4 query URLs.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { createNeonStorage } from '@avelonjs/neon'
|
|
170
|
+
|
|
171
|
+
const disk = createNeonStorage({
|
|
172
|
+
bucket: process.env.NEON_S3_BUCKET,
|
|
173
|
+
instance: 'uploads',
|
|
174
|
+
})
|
|
175
|
+
await disk.put('avatars/me.bin', new Uint8Array([1, 2, 3]), {
|
|
176
|
+
contentType: 'application/octet-stream',
|
|
177
|
+
})
|
|
178
|
+
const url = await disk.signedUrl('avatars/me.bin', 60)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Transforms are undeclared in v1 (D28). Signed uploads and listing are deferred.
|
|
182
|
+
|
|
183
|
+
## Queue
|
|
184
|
+
|
|
185
|
+
Durable jobs use Postgres `FOR UPDATE SKIP LOCKED` on the Neon database.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { createNeonQueue } from '@avelonjs/neon'
|
|
189
|
+
|
|
190
|
+
const queue = createNeonQueue({
|
|
191
|
+
url: process.env.NEON_DATABASE_URL,
|
|
192
|
+
})
|
|
193
|
+
const id = await queue.enqueue({ name: 'GenerateReport', payload: { reportId: 'report-1' } })
|
|
194
|
+
await queue.drain(async (receipt) => {
|
|
195
|
+
if (receipt.id !== id) return
|
|
196
|
+
})
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Method Reference
|
|
200
|
+
|
|
201
|
+
| Method / export | Signature | Description |
|
|
202
|
+
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
203
|
+
| `createNeonDatabase` | `(options?: NeonDatabaseOptions) => NeonDatabase` | Constructs the Neon database driver. |
|
|
204
|
+
| `NeonDatabase` | `class NeonDatabase` | Neon database implementation wrapping the Postgres Query IR compiler. |
|
|
205
|
+
| `NeonDatabase.execute` | `(query: QueryIR) => Promise<QueryResult>` | Compiles and executes one query IR operation. |
|
|
206
|
+
| `NeonDatabase.rpc` | `(routine: string, args: Readonly<Record<string, unknown>>) => Promise<T>` | Invokes a Postgres routine; missing routines raise `Invalid`. |
|
|
207
|
+
| `NeonDatabase.plan` | `() => Promise<MigrationPlan>` | Returns pending migration identifiers and SQL steps. |
|
|
208
|
+
| `NeonDatabase.apply` | `() => Promise<readonly MigrationStatus[]>` | Applies pending migrations. |
|
|
209
|
+
| `NeonDatabase.rollback` | `(steps?: number) => Promise<readonly MigrationStatus[]>` | Rolls back the newest applied migration batches. |
|
|
210
|
+
| `NeonDatabase.status` | `() => Promise<readonly MigrationStatus[]>` | Lists applied and pending migration states. |
|
|
211
|
+
| `NeonDatabase.transaction` | `(callback) => Promise<T>` | Runs a callback inside a Postgres transaction. |
|
|
212
|
+
| `NeonDatabase.resetFixtures` | `() => Promise<void>` | Recreates assay fixtures used by live conformance. |
|
|
213
|
+
| `NeonDatabase.raw` | `() => NeonSql` | Returns the `@neondatabase/serverless` query function. |
|
|
214
|
+
| `NeonDatabase.close` | `() => Promise<void>` | Closes the underlying SQL client pool. |
|
|
215
|
+
| `neonDatabaseCapabilities` | `const` | Literal database capability declaration. |
|
|
216
|
+
| `NeonDatabaseOptions` | `interface` | Construction options for the database driver. |
|
|
217
|
+
| `NeonSql` | `type` | Vendor client returned by `NeonDatabase.raw()`. |
|
|
218
|
+
| `PostgresMigration` | `interface` | Driver-owned `id`, `up`, and `down` SQL pair. |
|
|
219
|
+
| `createNeonIdentity` | `(options?: NeonIdentityOptions) => (cookies: RequestCookies) => NeonIdentity` | Returns the pinned config-time identity factory. |
|
|
220
|
+
| `NeonIdentity` | `class NeonIdentity` | Neon Auth identity implementation. |
|
|
221
|
+
| `NeonIdentity.user` | `() => Promise<NeonActor \| null>` | Returns the current actor from the request cookie session. |
|
|
222
|
+
| `NeonIdentity.session` | `() => Promise<NeonSession \| null>` | Returns the current session or null. |
|
|
223
|
+
| `NeonIdentity.register` | `(email: string, password: string) => Promise<NeonActor>` | Registers and establishes a session cookie. |
|
|
224
|
+
| `NeonIdentity.signInWithPassword` | `(email: string, password: string) => Promise<NeonActor>` | Signs in and writes the session cookie. |
|
|
225
|
+
| `NeonIdentity.signOut` | `() => Promise<void>` | Ends the Auth session and clears the cookie. |
|
|
226
|
+
| `NeonIdentity.sendPasswordReset` | `(email: string) => Promise<void>` | Sends a recovery request without revealing account existence. |
|
|
227
|
+
| `NeonIdentity.resetPassword` | `(token: string, password: string) => Promise<void>` | Replaces a password after validating a recovery token. |
|
|
228
|
+
| `NeonIdentity.updatePassword` | `(password: string) => Promise<void>` | Changes the current actor's password. |
|
|
229
|
+
| `NeonIdentity.sendMagicLink` | `(email: string, redirectTo?: string) => Promise<void>` | Sends a passwordless sign-in link. |
|
|
230
|
+
| `NeonIdentity.signInWithMagicLink` | `(token: string) => Promise<NeonActor>` | Redeems a link token for a session. |
|
|
231
|
+
| `NeonIdentity.challengeMfa` | `(factor?: 'totp') => Promise<MfaChallenge<'totp'>>` | Begins a TOTP challenge for the current session. |
|
|
232
|
+
| `NeonIdentity.verifyMfa` | `(challengeId: string, code: string) => Promise<void>` | Verifies a TOTP challenge response. |
|
|
233
|
+
| `NeonIdentity.sendEmailVerification` | `(email?: string) => Promise<void>` | Sends a confirmation message without revealing account existence. |
|
|
234
|
+
| `NeonIdentity.verifyEmail` | `(token: string) => Promise<void>` | Confirms an email address after validating a vendor token. |
|
|
235
|
+
| `NeonIdentity.raw` | `() => { authUrl: string }` | Returns the Auth HTTP root at the vendor boundary. |
|
|
236
|
+
| `neonIdentityCapabilities` | `const` | Literal identity capability declaration. |
|
|
237
|
+
| `NeonIdentityOptions` | `interface` | Construction options for the identity factory. |
|
|
238
|
+
| `NeonActor` | `interface` | Actor payload with `id` and `email`. |
|
|
239
|
+
| `NeonSession` | `interface` | Cookie session with access and refresh tokens. |
|
|
240
|
+
| `readSessionCookie` | `(cookies: RequestCookies, cookieName: string) => NeonSession \| null` | Parses the request-scoped session cookie. |
|
|
241
|
+
| `writeSessionCookie` | `(cookies: RequestCookies, cookieName: string, session: NeonSession) => void` | Writes the session cookie for the current request. |
|
|
242
|
+
| `clearSessionCookie` | `(cookies: RequestCookies, cookieName: string) => void` | Deletes the session cookie for the current request. |
|
|
243
|
+
| `DEFAULT_SESSION_COOKIE` | `string` | Default session cookie name. |
|
|
244
|
+
| `LocalAuthServer` | `class LocalAuthServer` | Better Auth-shaped fixture for identity conformance. |
|
|
245
|
+
| `LocalAuthServer.start` | `() => Promise<string>` | Starts the fixture on an ephemeral port. |
|
|
246
|
+
| `LocalAuthServer.reset` | `() => void` | Clears users, sessions, and recovery tokens. |
|
|
247
|
+
| `LocalAuthServer.stop` | `() => Promise<void>` | Stops the fixture listener. |
|
|
248
|
+
| `LocalAuthServer.recoveryToken` | `(email: string) => string \| undefined` | Returns the recovery token issued for an email. |
|
|
249
|
+
| `LocalAuthServer.magicLinkToken` | `(email: string) => string \| undefined` | Returns the magic-link token issued for an email. |
|
|
250
|
+
| `NEON_IDENTITY_ERROR_MAP` | `readonly { code: string; framework: string; meaning: string }[]` | Better Auth error codes and the taxonomy member each becomes. |
|
|
251
|
+
| `LocalAuthServer.verificationToken` | `(email: string) => string \| undefined` | Returns the email confirmation token issued for an email. |
|
|
252
|
+
| `createNeonSocial` | `(options?: NeonSocialOptions) => NeonSocial` | Constructs the Neon Auth social driver. |
|
|
253
|
+
| `NeonSocial` | `class NeonSocial` | Neon Auth social implementation. |
|
|
254
|
+
| `NeonSocial.redirect` | `(provider: string, callbackUrl: string, state?: string) => Promise<string>` | Builds a Better Auth authorize URL and records CSRF state. |
|
|
255
|
+
| `NeonSocial.callback` | `(provider: string, params: Readonly<Record<string, string>>, callbackUrl: string) => Promise<SocialIdentity>` | Verifies state and exchanges the authorization code. |
|
|
256
|
+
| `NeonSocial.raw` | `() => { authUrl: string }` | Returns the Auth HTTP root at the vendor boundary. |
|
|
257
|
+
| `neonSocialCapabilities` | `const` | Literal social provider declaration. |
|
|
258
|
+
| `NeonSocialOptions` | `interface` | Construction options for the social driver. |
|
|
259
|
+
| `NeonSocialProfile` | `interface` | Normalized provider profile. |
|
|
260
|
+
| `LocalSocialServer` | `class LocalSocialServer` | Better Auth-shaped token fixture for social conformance. |
|
|
261
|
+
| `LocalSocialServer.start` | `() => Promise<string>` | Starts the social fixture on an ephemeral port. |
|
|
262
|
+
| `LocalSocialServer.stop` | `() => Promise<void>` | Stops the social fixture listener. |
|
|
263
|
+
| `createNeonTokens` | `(options?: NeonTokenOptions) => NeonTokens` | Constructs the Postgres-backed Signet driver. |
|
|
264
|
+
| `NeonTokens` | `class NeonTokens` | Neon Signet implementation. |
|
|
265
|
+
| `NeonTokens.issue` | `(name: string, options?: TokenIssueOptions) => Promise<IssuedToken>` | Issues a named token and returns plaintext once. |
|
|
266
|
+
| `NeonTokens.verify` | `(plainText: string) => Promise<TokenRecord>` | Authenticates a plaintext token or raises `Unauthenticated`. |
|
|
267
|
+
| `NeonTokens.list` | `() => Promise<readonly TokenRecord[]>` | Lists metadata for the current subject without plaintext. |
|
|
268
|
+
| `NeonTokens.revoke` | `(id: string) => Promise<void>` | Revokes one token by stable identifier. |
|
|
269
|
+
| `NeonTokens.reset` | `() => Promise<void>` | Recreates the empty signet table. |
|
|
270
|
+
| `NeonTokens.raw` | `() => SQL` | Returns the Bun SQL client at the vendor boundary. |
|
|
271
|
+
| `NeonTokens.close` | `() => Promise<void>` | Closes the SQL client pool. |
|
|
272
|
+
| `neonTokenCapabilities` | `const` | Literal token capability declaration. |
|
|
273
|
+
| `NeonTokenOptions` | `interface` | Construction options for the token driver. |
|
|
274
|
+
| `createNeonStorage` | `(options?: NeonStorageOptions) => NeonStorage` | Constructs the S3 storage driver. |
|
|
275
|
+
| `NeonStorage` | `class NeonStorage` | S3 storage implementation. |
|
|
276
|
+
| `NeonStorage.put` | `(path: string, contents: Uint8Array \| AsyncIterable<Uint8Array>, options?: { contentType?: string }) => Promise<StorageObject>` | Stores bytes and returns metadata. |
|
|
277
|
+
| `NeonStorage.get` | `(path: string) => Promise<Uint8Array>` | Reads object bytes or raises `NotFound`. |
|
|
278
|
+
| `NeonStorage.delete` | `(path: string) => Promise<void>` | Deletes an object if it exists. |
|
|
279
|
+
| `NeonStorage.exists` | `(path: string) => Promise<boolean>` | Reports whether an object exists. |
|
|
280
|
+
| `NeonStorage.signedUrl` | `(path: string, expiresInSeconds: number) => Promise<string>` | Creates a SigV4 signed read URL. |
|
|
281
|
+
| `NeonStorage.raw` | `() => S3Client` | Returns the AWS S3 client at the vendor boundary. |
|
|
282
|
+
| `neonStorageCapabilities` | `const` | Literal storage capability declaration. |
|
|
283
|
+
| `NeonStorageOptions` | `interface` | Construction options for the storage driver. |
|
|
284
|
+
| `LocalS3Server` | `class LocalS3Server` | Path-style S3 fixture for storage conformance. |
|
|
285
|
+
| `LocalS3Server.start` | `() => Promise<string>` | Starts the S3 fixture on an ephemeral port. |
|
|
286
|
+
| `LocalS3Server.reset` | `() => void` | Clears stored objects. |
|
|
287
|
+
| `LocalS3Server.stop` | `() => Promise<void>` | Stops the S3 fixture listener. |
|
|
288
|
+
| `signedUrlExpiryMs` | `(url: URL) => number \| undefined` | Parses SigV4 query expiry to epoch milliseconds. |
|
|
289
|
+
| `createNeonQueue` | `(options?: NeonQueueOptions) => NeonQueue` | Constructs the skip-locked Postgres queue driver. |
|
|
290
|
+
| `NeonQueue` | `class NeonQueue` | Neon queue implementation. |
|
|
291
|
+
| `NeonQueue.enqueue` | `(job: QueueJob) => Promise<string>` | Enqueues a job for immediate delivery. |
|
|
292
|
+
| `NeonQueue.enqueueAt` | `(job: QueueJob, availableAt: Date) => Promise<string>` | Enqueues a job no earlier than the supplied time. |
|
|
293
|
+
| `NeonQueue.drain` | `(handler: (receipt: QueueReceipt) => Promise<void>, options?: { queue?: string; limit?: number }) => Promise<number>` | Delivers available jobs and counts attempts. |
|
|
294
|
+
| `NeonQueue.retry` | `(id: string, delaySeconds?: number) => Promise<void>` | Releases a failed job, optionally after a delay. |
|
|
295
|
+
| `NeonQueue.failed` | `(queue?: string) => Promise<readonly FailedQueueJob[]>` | Lists retained terminal failures. |
|
|
296
|
+
| `NeonQueue.replay` | `(id: string) => Promise<void>` | Replays a terminal failure with a fresh attempt budget. |
|
|
297
|
+
| `NeonQueue.forget` | `(id: string) => Promise<void>` | Permanently removes a terminal failure. |
|
|
298
|
+
| `NeonQueue.reset` | `() => Promise<void>` | Recreates empty job tables. |
|
|
299
|
+
| `NeonQueue.raw` | `() => SQL` | Returns the Bun SQL client at the vendor boundary. |
|
|
300
|
+
| `NeonQueue.close` | `() => Promise<void>` | Closes the SQL client pool. |
|
|
301
|
+
| `neonQueueCapabilities` | `const` | Literal queue capability declaration. |
|
|
302
|
+
| `NeonQueueOptions` | `interface` | Construction options for the queue driver. |
|
|
303
|
+
|
|
304
|
+
## Testing
|
|
305
|
+
|
|
306
|
+
Run the shared suites. Identity and social use Better Auth-shaped local servers. Storage uses `LocalS3Server` so SigV4 signed reads stay fetchable without AWS. Database, tokens, and queue require a reachable Neon or Postgres URL and fail closed when it is missing.
|
|
307
|
+
|
|
308
|
+
The database driver talks to Neon's HTTP and WebSocket endpoints, which a plain Postgres does not serve. When the configured URL is not a `neon.tech` host, `tests/neon-local.ts` starts both endpoints locally in front of that Postgres, so live conformance exercises the real vendor client end to end.
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { identitySuite, storageSuite } from '@avelonjs/conformance/suites'
|
|
312
|
+
import {
|
|
313
|
+
createNeonIdentity,
|
|
314
|
+
createNeonStorage,
|
|
315
|
+
LocalAuthServer,
|
|
316
|
+
LocalS3Server,
|
|
317
|
+
} from '@avelonjs/neon'
|
|
318
|
+
|
|
319
|
+
const auth = new LocalAuthServer()
|
|
320
|
+
const authUrl = await auth.start()
|
|
321
|
+
identitySuite({
|
|
322
|
+
name: 'neon identity',
|
|
323
|
+
create: () => {
|
|
324
|
+
auth.reset()
|
|
325
|
+
return createNeonIdentity({ authUrl })
|
|
326
|
+
},
|
|
327
|
+
recoveryToken: async (email) => {
|
|
328
|
+
const token = auth.recoveryToken(email)
|
|
329
|
+
if (token === undefined) throw new Error(`No recovery token for ${email}`)
|
|
330
|
+
return token
|
|
331
|
+
},
|
|
332
|
+
emailVerificationToken: async (email) => {
|
|
333
|
+
const token = auth.verificationToken(email)
|
|
334
|
+
if (token === undefined) throw new Error(`No verification token for ${email}`)
|
|
335
|
+
return token
|
|
336
|
+
},
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
const s3 = new LocalS3Server()
|
|
340
|
+
const endpoint = await s3.start()
|
|
341
|
+
storageSuite({
|
|
342
|
+
name: 'neon storage',
|
|
343
|
+
create: () => createNeonStorage({ endpoint, bucket: 'assay' }),
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
```sh
|
|
348
|
+
bun test
|
|
349
|
+
bun run typecheck
|
|
350
|
+
reeve docs:check
|
|
351
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@avelonjs/neon",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Neon drivers for Avelon's database, identity, social, tokens, storage, and queue contracts.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Ryan Yannelli <ryanyannelli@gmail.com>",
|
|
8
|
+
"homepage": "https://github.com/yannelli/avelon",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/yannelli/avelon.git",
|
|
12
|
+
"directory": "packages/neon"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/yannelli/avelon/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"avelon",
|
|
19
|
+
"typescript",
|
|
20
|
+
"neon",
|
|
21
|
+
"postgres",
|
|
22
|
+
"s3"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"exports": {
|
|
34
|
+
".": "./src/index.ts",
|
|
35
|
+
"./database": "./src/database/index.ts"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "bun test",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@avelonjs/core": "workspace:*",
|
|
43
|
+
"@avelonjs/postgres": "workspace:*",
|
|
44
|
+
"@aws-sdk/client-s3": "3.899.0",
|
|
45
|
+
"@aws-sdk/s3-request-presigner": "3.899.0",
|
|
46
|
+
"@neondatabase/serverless": "1.0.2"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@avelonjs/conformance": "workspace:*",
|
|
50
|
+
"@types/bun": "1.3.14",
|
|
51
|
+
"typescript": "5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"bun": ">=1.3.14"
|
|
55
|
+
}
|
|
56
|
+
}
|