ras-stack 0.1.0 → 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/README.md +92 -37
- package/dist/auth/providers.d.ts +1 -0
- package/dist/auth/providers.js +9 -0
- package/dist/auth/providers.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,53 +1,69 @@
|
|
|
1
1
|
# ras-stack
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**The small pieces of full-stack plumbing I reuse across my TypeScript applications.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/ras-stack) [](https://github.com/richardsolomou/ras-stack/actions/workflows/ci.yml) [](LICENSE)
|
|
6
|
+
|
|
7
|
+
I build several applications with the same TypeScript stack. They kept growing slightly different copies of the same code for session settings, origin checks, email delivery, realtime publication, resumable uploads, health checks, and project configuration.
|
|
8
|
+
|
|
9
|
+
`ras-stack` is the shared home for that plumbing. It is a personal library, published in case the code or the way it is split up is useful to someone else.
|
|
10
|
+
|
|
11
|
+
## What it is
|
|
12
|
+
|
|
13
|
+
The package contains independent helpers for common application infrastructure:
|
|
14
|
+
|
|
15
|
+
- **Authentication:** secure defaults and utilities for sessions, rate limits, secrets, social providers, tokens, and trusted origins.
|
|
16
|
+
- **Server requests:** same-origin mutation guards, error-normalizing RPC wrappers, canonical-host redirects, and health responses.
|
|
17
|
+
- **Realtime:** Centrifugo token signing and a bounded publisher with retries and graceful shutdown.
|
|
18
|
+
- **Email:** SMTP environment parsing and a small Nodemailer delivery interface.
|
|
19
|
+
- **Uploads:** a promise-based wrapper around resumable `tus-js-client` uploads.
|
|
20
|
+
- **Project configuration:** shared TypeScript and Oxlint bases.
|
|
21
|
+
|
|
22
|
+
Each area has its own import path. An application can use one without adopting the rest.
|
|
23
|
+
|
|
24
|
+
## What it is not
|
|
25
|
+
|
|
26
|
+
This is not a framework, starter, application template, or complete authentication system. It does not own an application's database schema, migrations, routes, authorization rules, email templates, upload policy, or realtime event names.
|
|
27
|
+
|
|
28
|
+
The libraries underneath remain available normally. Applications still configure and call Better Auth, TanStack Start, Drizzle, Nodemailer, `tus-js-client`, and Centrifugo directly. The helpers only centralize the parts that would otherwise be copied unchanged.
|
|
6
29
|
|
|
7
30
|
## Install
|
|
8
31
|
|
|
32
|
+
`ras-stack` requires Node 24.
|
|
33
|
+
|
|
9
34
|
```sh
|
|
10
35
|
pnpm add ras-stack
|
|
11
36
|
```
|
|
12
37
|
|
|
13
|
-
|
|
38
|
+
Nodemailer and `tus-js-client` are optional peer dependencies. Install one only when using its entrypoint:
|
|
14
39
|
|
|
15
40
|
```sh
|
|
16
41
|
pnpm add nodemailer
|
|
17
42
|
pnpm add tus-js-client
|
|
18
43
|
```
|
|
19
44
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- `ras-stack/auth` — secrets, origin policy, provider credentials, random tokens, sessions, and rate limits.
|
|
23
|
-
- `ras-stack/email` — SMTP environment parsing, Nodemailer transport creation, and delivery.
|
|
24
|
-
- `ras-stack/realtime` — Centrifugo publication, bounded retries/backpressure, shutdown, and signed tokens.
|
|
25
|
-
- `ras-stack/server` — canonical redirects, health responses, and framework-injected RPC wrappers.
|
|
26
|
-
- `ras-stack/uploads` — configurable tus uploads and error-response parsing.
|
|
27
|
-
- `ras-stack/config/*` — inheritable Oxlint and TypeScript configuration.
|
|
28
|
-
|
|
29
|
-
## Auth
|
|
45
|
+
## Authentication and request security
|
|
30
46
|
|
|
31
|
-
The auth entrypoint provides
|
|
47
|
+
The auth entrypoint provides options and utilities rather than an auth factory. The application keeps its complete Better Auth configuration:
|
|
32
48
|
|
|
33
49
|
```ts
|
|
34
|
-
import { standardRateLimitOptions, standardSessionOptions, trustedOrigins } from 'ras-stack/auth'
|
|
35
50
|
import { betterAuth } from 'better-auth'
|
|
51
|
+
import { configuredProviderOptions, standardRateLimitOptions, standardSessionOptions, trustedOrigins } from 'ras-stack/auth'
|
|
36
52
|
|
|
37
53
|
const auth = betterAuth({
|
|
38
54
|
database,
|
|
39
55
|
plugins,
|
|
56
|
+
socialProviders: configuredProviderOptions(['google', 'discord']),
|
|
40
57
|
session: standardSessionOptions(),
|
|
41
58
|
rateLimit: standardRateLimitOptions({ '/sign-up/email': { window: 60, max: 10 } }),
|
|
42
|
-
trustedOrigins: trustedOrigins({
|
|
59
|
+
trustedOrigins: trustedOrigins({
|
|
60
|
+
configured: [process.env.APP_URL],
|
|
61
|
+
trustForwardedHeaders: true,
|
|
62
|
+
}),
|
|
43
63
|
})
|
|
44
64
|
```
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
## Server functions
|
|
49
|
-
|
|
50
|
-
Framework access is injected, leaving TanStack Start available normally:
|
|
66
|
+
Framework access is injected into server helpers, so the package does not need to wrap TanStack Start:
|
|
51
67
|
|
|
52
68
|
```ts
|
|
53
69
|
import { getRequest } from '@tanstack/react-start/server'
|
|
@@ -56,15 +72,19 @@ import { createRpc } from 'ras-stack/server'
|
|
|
56
72
|
|
|
57
73
|
export const { rpc, mutationRpc } = createRpc({
|
|
58
74
|
getRequest,
|
|
59
|
-
requireMutation: (request) =>
|
|
75
|
+
requireMutation: (request) =>
|
|
76
|
+
requireSameOrigin(request, {
|
|
77
|
+
configured: [process.env.APP_URL],
|
|
78
|
+
trustForwardedHeaders: true,
|
|
79
|
+
}),
|
|
60
80
|
})
|
|
61
81
|
```
|
|
62
82
|
|
|
63
|
-
Only enable `trustForwardedHeaders`
|
|
83
|
+
Only enable `trustForwardedHeaders` behind a proxy that replaces incoming forwarded headers. Otherwise a client could choose the origin used by the check.
|
|
64
84
|
|
|
65
|
-
## Realtime
|
|
85
|
+
## Realtime updates
|
|
66
86
|
|
|
67
|
-
|
|
87
|
+
Applications choose their channel names, authorize subscriptions, and define payloads. `ras-stack` handles Centrifugo's HTTP publication and signed tokens:
|
|
68
88
|
|
|
69
89
|
```ts
|
|
70
90
|
import { CentrifugoPublisher, signRealtimeToken } from 'ras-stack/realtime'
|
|
@@ -76,6 +96,7 @@ const publisher = new CentrifugoPublisher({
|
|
|
76
96
|
maxPendingChannels: 1024,
|
|
77
97
|
onError: (error, channel) => logger.error({ error, channel }, 'realtime publication failed'),
|
|
78
98
|
})
|
|
99
|
+
|
|
79
100
|
publisher.publish(`battle:${battle.id}`, { type: 'change' })
|
|
80
101
|
|
|
81
102
|
const token = signRealtimeToken(user.id, { channel: `battle:${battle.id}`, info: presence }, { secret })
|
|
@@ -83,11 +104,11 @@ const token = signRealtimeToken(user.id, { channel: `battle:${battle.id}`, info:
|
|
|
83
104
|
await publisher.close()
|
|
84
105
|
```
|
|
85
106
|
|
|
86
|
-
`publish()` returns `false` when the publisher is closed, disabled, or at capacity. `close()` rejects new work and waits for accepted publications
|
|
107
|
+
`publish()` returns `false` when the publisher is closed, disabled, or at capacity. `close()` rejects new work and waits for accepted publications and their bounded retries to finish.
|
|
87
108
|
|
|
88
109
|
## Email and uploads
|
|
89
110
|
|
|
90
|
-
|
|
111
|
+
The optional integrations return the underlying library objects when an application needs more control:
|
|
91
112
|
|
|
92
113
|
```ts
|
|
93
114
|
import { createSmtpDelivery, createSmtpTransport, smtpConfigFromEnvironment } from 'ras-stack/email'
|
|
@@ -103,14 +124,15 @@ const upload = createTusUpload({
|
|
|
103
124
|
shouldRetry: (status) => status !== 423,
|
|
104
125
|
onProgress,
|
|
105
126
|
})
|
|
127
|
+
|
|
106
128
|
await startTusUpload(upload)
|
|
107
129
|
```
|
|
108
130
|
|
|
109
|
-
|
|
131
|
+
Applications retain ownership of email templates, missing-email behavior, upload metadata, authorization, quotas, and completion processing.
|
|
110
132
|
|
|
111
|
-
## Shared configuration
|
|
133
|
+
## Shared project configuration
|
|
112
134
|
|
|
113
|
-
|
|
135
|
+
Extend the supplied configuration and override anything specific to the application:
|
|
114
136
|
|
|
115
137
|
```json
|
|
116
138
|
{
|
|
@@ -128,21 +150,54 @@ Oxlint and TypeScript configurations are inheritable:
|
|
|
128
150
|
}
|
|
129
151
|
```
|
|
130
152
|
|
|
153
|
+
TypeScript bases are also available at `ras-stack/config/typescript/browser` and `ras-stack/config/typescript/library`.
|
|
154
|
+
|
|
131
155
|
## GitHub Actions
|
|
132
156
|
|
|
133
|
-
The
|
|
157
|
+
The JavaScript setup action reads the Node version from `engines.node` and the pnpm version from `packageManager` in the consuming repository:
|
|
134
158
|
|
|
135
159
|
```yaml
|
|
136
160
|
steps:
|
|
137
161
|
- uses: actions/checkout@v7
|
|
138
|
-
- uses: richardsolomou/ras-stack/actions/setup-js@v0.
|
|
139
|
-
with:
|
|
140
|
-
just-version: '1.58.0'
|
|
162
|
+
- uses: richardsolomou/ras-stack/actions/setup-js@v0.2.0
|
|
141
163
|
- run: pnpm check
|
|
142
164
|
```
|
|
143
165
|
|
|
144
|
-
|
|
166
|
+
Just is independent of the application language and is installed separately when a repository uses it:
|
|
167
|
+
|
|
168
|
+
```yaml
|
|
169
|
+
- uses: richardsolomou/ras-stack/actions/setup-just@v0.2.0
|
|
170
|
+
with:
|
|
171
|
+
version: '1.58.0'
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Applications using Changesets can call the reusable release workflow after their own required checks:
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
release:
|
|
178
|
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
179
|
+
needs: [check]
|
|
180
|
+
permissions:
|
|
181
|
+
contents: write
|
|
182
|
+
uses: richardsolomou/ras-stack/.github/workflows/release-changesets.yml@v0.2.0
|
|
183
|
+
secrets: inherit
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The workflow consumes pending changesets, commits the resulting versions and changelogs, pushes the commit and tag atomically, and creates a GitHub Release. It does nothing when no versioned changeset is present. The caller owns its checks, Changesets configuration, release policy, and any deployment that follows the release.
|
|
187
|
+
|
|
188
|
+
Pin actions and reusable workflows to a release tag and let Dependabot propose upgrades.
|
|
189
|
+
|
|
190
|
+
## Development
|
|
191
|
+
|
|
192
|
+
Development requires Node 24 and pnpm 11.15.0.
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
pnpm install
|
|
196
|
+
pnpm check
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for release instructions. Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
|
|
145
200
|
|
|
146
|
-
## License
|
|
201
|
+
## License
|
|
147
202
|
|
|
148
|
-
|
|
203
|
+
[GNU Affero General Public License v3.0](LICENSE).
|
package/dist/auth/providers.d.ts
CHANGED
|
@@ -4,3 +4,4 @@ export type ProviderCredentials = {
|
|
|
4
4
|
};
|
|
5
5
|
export declare function configuredProviders<const Provider extends string>(providers: readonly Provider[], environment?: NodeJS.ProcessEnv): Provider[];
|
|
6
6
|
export declare function providerCredentials(provider: string, environment?: NodeJS.ProcessEnv): ProviderCredentials | undefined;
|
|
7
|
+
export declare function configuredProviderOptions<const Provider extends string>(providers: readonly Provider[], environment?: NodeJS.ProcessEnv): Partial<Record<Provider, ProviderCredentials>>;
|
package/dist/auth/providers.js
CHANGED
|
@@ -10,4 +10,13 @@ export function providerCredentials(provider, environment = process.env) {
|
|
|
10
10
|
const clientSecret = environment[`${prefix}_CLIENT_SECRET`]?.trim();
|
|
11
11
|
return clientId && clientSecret ? { clientId, clientSecret } : undefined;
|
|
12
12
|
}
|
|
13
|
+
export function configuredProviderOptions(providers, environment = process.env) {
|
|
14
|
+
const options = {};
|
|
15
|
+
for (const provider of providers) {
|
|
16
|
+
const credentials = providerCredentials(provider, environment);
|
|
17
|
+
if (credentials)
|
|
18
|
+
options[provider] = credentials;
|
|
19
|
+
}
|
|
20
|
+
return options;
|
|
21
|
+
}
|
|
13
22
|
//# sourceMappingURL=providers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/auth/providers.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,mBAAmB,CACjC,SAA8B,EAC9B,WAAW,GAAsB,OAAO,CAAC,GAAG;IAE5C,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE;QACnC,MAAM,MAAM,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QAC1D,OAAO,OAAO,CAAC,WAAW,CAAC,GAAG,MAAM,YAAY,CAAC,EAAE,IAAI,EAAE,IAAI,WAAW,CAAC,GAAG,MAAM,gBAAgB,CAAC,EAAE,IAAI,EAAE,CAAC,CAAA;IAC9G,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,QAAgB,EAAE,WAAW,GAAsB,OAAO,CAAC,GAAG;IAChG,MAAM,MAAM,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAC1D,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,MAAM,YAAY,CAAC,EAAE,IAAI,EAAE,CAAA;IAC3D,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,MAAM,gBAAgB,CAAC,EAAE,IAAI,EAAE,CAAA;IACnE,OAAO,QAAQ,IAAI,YAAY,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AAC1E,CAAC"}
|
|
1
|
+
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../../src/auth/providers.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,mBAAmB,CACjC,SAA8B,EAC9B,WAAW,GAAsB,OAAO,CAAC,GAAG;IAE5C,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE;QACnC,MAAM,MAAM,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QAC1D,OAAO,OAAO,CAAC,WAAW,CAAC,GAAG,MAAM,YAAY,CAAC,EAAE,IAAI,EAAE,IAAI,WAAW,CAAC,GAAG,MAAM,gBAAgB,CAAC,EAAE,IAAI,EAAE,CAAC,CAAA;IAC9G,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,QAAgB,EAAE,WAAW,GAAsB,OAAO,CAAC,GAAG;IAChG,MAAM,MAAM,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAC1D,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,MAAM,YAAY,CAAC,EAAE,IAAI,EAAE,CAAA;IAC3D,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,MAAM,gBAAgB,CAAC,EAAE,IAAI,EAAE,CAAA;IACnE,OAAO,QAAQ,IAAI,YAAY,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AAC1E,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,SAA8B,EAC9B,WAAW,GAAsB,OAAO,CAAC,GAAG;IAE5C,MAAM,OAAO,GAAmD,EAAE,CAAA;IAClE,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QACjC,MAAM,WAAW,GAAG,mBAAmB,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAA;QAC9D,IAAI,WAAW;YAAE,OAAO,CAAC,QAAQ,CAAC,GAAG,WAAW,CAAA;IAClD,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ras-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Composable full-stack primitives shared across Richard Solomou's applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authentication",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"license": "AGPL-3.0-only",
|
|
15
15
|
"repository": {
|
|
16
16
|
"type": "git",
|
|
17
|
-
"url": "https://github.com/richardsolomou/ras-stack.git"
|
|
17
|
+
"url": "git+https://github.com/richardsolomou/ras-stack.git"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"dist",
|