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 CHANGED
@@ -1,53 +1,69 @@
1
1
  # ras-stack
2
2
 
3
- Composable full-stack primitives shared across Richard Solomou's applications.
3
+ **The small pieces of full-stack plumbing I reuse across my TypeScript applications.**
4
4
 
5
- `ras-stack` removes repeated infrastructure decisions without wrapping or replacing the libraries underneath. Applications continue to call Better Auth, TanStack Start, Drizzle, React Query, and Centrifugo directly. They can adopt any primitive independently and retain ownership of schemas, migrations, authorization, plugins, routes, and domain events.
5
+ [![npm](https://img.shields.io/npm/v/ras-stack)](https://www.npmjs.com/package/ras-stack) [![Build](https://img.shields.io/github/actions/workflow/status/richardsolomou/ras-stack/ci.yml?branch=main)](https://github.com/richardsolomou/ras-stack/actions/workflows/ci.yml) [![License](https://img.shields.io/github/license/richardsolomou/ras-stack)](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
- The email and upload entrypoints use optional peer dependencies. Install only the integration an application consumes:
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
- The package requires Node 24 and publishes separate entrypoints so an application does not load unused integrations:
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 independent options and utilities rather than an auth factory:
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({ configured: [process.env.APP_URL], trustForwardedHeaders: true }),
59
+ trustedOrigins: trustedOrigins({
60
+ configured: [process.env.APP_URL],
61
+ trustForwardedHeaders: true,
62
+ }),
43
63
  })
44
64
  ```
45
65
 
46
- The application supplies its normal Better Auth configuration, including its database adapter, schema, plugins, callbacks, and product-specific policy.
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) => requireSameOrigin(request, { configured: [process.env.APP_URL], trustForwardedHeaders: true }),
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` when the application is deployed behind a proxy that replaces incoming forwarded headers.
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
- The realtime entrypoint owns Centrifugo transport mechanics and token signing. Applications own channel names, authorization, presence information, and publication payloads:
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 to finish, including the bounded retry budget.
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
- Optional entrypoints integrate with dependencies that remain installed and available to the application:
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
- `createSmtpTransport()` and `createTusUpload()` return the upstream objects, so applications can use capabilities the convenience wrappers do not cover. Applications retain ownership of email templates, missing-email behavior, upload metadata, authorization, quotas, and completion processing.
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
- Oxlint and TypeScript configurations are inheritable:
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 shared setup action reads Node from `engines.node` and pnpm from `packageManager` in the consuming repository's `package.json`:
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.1.0
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
- Pin the action to an immutable release tag and let Dependabot propose upgrades.
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 and security
201
+ ## License
147
202
 
148
- `ras-stack` is licensed under the GNU Affero General Public License v3.0. Report vulnerabilities through GitHub private vulnerability reporting as described in [SECURITY.md](SECURITY.md).
203
+ [GNU Affero General Public License v3.0](LICENSE).
@@ -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>>;
@@ -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.1.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",