ras-stack 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 +661 -0
- package/README.md +148 -0
- package/config/oxlint.json +19 -0
- package/config/typescript/browser.json +15 -0
- package/config/typescript/library.json +13 -0
- package/config/typescript/tanstack.json +6 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/origins.d.ts +11 -0
- package/dist/auth/origins.js +35 -0
- package/dist/auth/origins.js.map +1 -0
- package/dist/auth/providers.d.ts +6 -0
- package/dist/auth/providers.js +13 -0
- package/dist/auth/providers.js.map +1 -0
- package/dist/auth/random.d.ts +2 -0
- package/dist/auth/random.js +10 -0
- package/dist/auth/random.js.map +1 -0
- package/dist/auth/secret.d.ts +8 -0
- package/dist/auth/secret.js +45 -0
- package/dist/auth/secret.js.map +1 -0
- package/dist/auth/settings.d.ts +28 -0
- package/dist/auth/settings.js +18 -0
- package/dist/auth/settings.js.map +1 -0
- package/dist/email/index.d.ts +30 -0
- package/dist/email/index.js +44 -0
- package/dist/email/index.js.map +1 -0
- package/dist/realtime/index.d.ts +2 -0
- package/dist/realtime/index.js +3 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/realtime/publisher.d.ts +34 -0
- package/dist/realtime/publisher.js +176 -0
- package/dist/realtime/publisher.js.map +1 -0
- package/dist/realtime/tokens.d.ts +6 -0
- package/dist/realtime/tokens.js +13 -0
- package/dist/realtime/tokens.js.map +1 -0
- package/dist/server/canonical-host.d.ts +5 -0
- package/dist/server/canonical-host.js +15 -0
- package/dist/server/canonical-host.js.map +1 -0
- package/dist/server/health.d.ts +4 -0
- package/dist/server/health.js +10 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/rpc.d.ts +13 -0
- package/dist/server/rpc.js +32 -0
- package/dist/server/rpc.js.map +1 -0
- package/dist/uploads/index.d.ts +30 -0
- package/dist/uploads/index.js +77 -0
- package/dist/uploads/index.js.map +1 -0
- package/package.json +91 -0
package/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# ras-stack
|
|
2
|
+
|
|
3
|
+
Composable full-stack primitives shared across Richard Solomou's applications.
|
|
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.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add ras-stack
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The email and upload entrypoints use optional peer dependencies. Install only the integration an application consumes:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pnpm add nodemailer
|
|
17
|
+
pnpm add tus-js-client
|
|
18
|
+
```
|
|
19
|
+
|
|
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
|
|
30
|
+
|
|
31
|
+
The auth entrypoint provides independent options and utilities rather than an auth factory:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { standardRateLimitOptions, standardSessionOptions, trustedOrigins } from 'ras-stack/auth'
|
|
35
|
+
import { betterAuth } from 'better-auth'
|
|
36
|
+
|
|
37
|
+
const auth = betterAuth({
|
|
38
|
+
database,
|
|
39
|
+
plugins,
|
|
40
|
+
session: standardSessionOptions(),
|
|
41
|
+
rateLimit: standardRateLimitOptions({ '/sign-up/email': { window: 60, max: 10 } }),
|
|
42
|
+
trustedOrigins: trustedOrigins({ configured: [process.env.APP_URL], trustForwardedHeaders: true }),
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
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:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { getRequest } from '@tanstack/react-start/server'
|
|
54
|
+
import { requireSameOrigin } from 'ras-stack/auth'
|
|
55
|
+
import { createRpc } from 'ras-stack/server'
|
|
56
|
+
|
|
57
|
+
export const { rpc, mutationRpc } = createRpc({
|
|
58
|
+
getRequest,
|
|
59
|
+
requireMutation: (request) => requireSameOrigin(request, { configured: [process.env.APP_URL], trustForwardedHeaders: true }),
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Only enable `trustForwardedHeaders` when the application is deployed behind a proxy that replaces incoming forwarded headers.
|
|
64
|
+
|
|
65
|
+
## Realtime
|
|
66
|
+
|
|
67
|
+
The realtime entrypoint owns Centrifugo transport mechanics and token signing. Applications own channel names, authorization, presence information, and publication payloads:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { CentrifugoPublisher, signRealtimeToken } from 'ras-stack/realtime'
|
|
71
|
+
|
|
72
|
+
const publisher = new CentrifugoPublisher({
|
|
73
|
+
apiUrl,
|
|
74
|
+
apiKey,
|
|
75
|
+
maxConcurrentChannels: 8,
|
|
76
|
+
maxPendingChannels: 1024,
|
|
77
|
+
onError: (error, channel) => logger.error({ error, channel }, 'realtime publication failed'),
|
|
78
|
+
})
|
|
79
|
+
publisher.publish(`battle:${battle.id}`, { type: 'change' })
|
|
80
|
+
|
|
81
|
+
const token = signRealtimeToken(user.id, { channel: `battle:${battle.id}`, info: presence }, { secret })
|
|
82
|
+
|
|
83
|
+
await publisher.close()
|
|
84
|
+
```
|
|
85
|
+
|
|
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.
|
|
87
|
+
|
|
88
|
+
## Email and uploads
|
|
89
|
+
|
|
90
|
+
Optional entrypoints integrate with dependencies that remain installed and available to the application:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { createSmtpDelivery, createSmtpTransport, smtpConfigFromEnvironment } from 'ras-stack/email'
|
|
94
|
+
import { createTusUpload, startTusUpload } from 'ras-stack/uploads'
|
|
95
|
+
|
|
96
|
+
const smtp = smtpConfigFromEnvironment()
|
|
97
|
+
const email = smtp ? createSmtpDelivery(smtp) : undefined
|
|
98
|
+
|
|
99
|
+
const upload = createTusUpload({
|
|
100
|
+
endpoint: '/api/upload',
|
|
101
|
+
file,
|
|
102
|
+
metadata,
|
|
103
|
+
shouldRetry: (status) => status !== 423,
|
|
104
|
+
onProgress,
|
|
105
|
+
})
|
|
106
|
+
await startTusUpload(upload)
|
|
107
|
+
```
|
|
108
|
+
|
|
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.
|
|
110
|
+
|
|
111
|
+
## Shared configuration
|
|
112
|
+
|
|
113
|
+
Oxlint and TypeScript configurations are inheritable:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"extends": ["./node_modules/ras-stack/config/oxlint.json"],
|
|
118
|
+
"rules": {
|
|
119
|
+
"application-specific-rule": "off"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"extends": "ras-stack/config/typescript/tanstack",
|
|
127
|
+
"include": ["src", "vite.config.ts"]
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## GitHub Actions
|
|
132
|
+
|
|
133
|
+
The shared setup action reads Node from `engines.node` and pnpm from `packageManager` in the consuming repository's `package.json`:
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
steps:
|
|
137
|
+
- uses: actions/checkout@v7
|
|
138
|
+
- uses: richardsolomou/ras-stack/actions/setup-js@v0.1.0
|
|
139
|
+
with:
|
|
140
|
+
just-version: '1.58.0'
|
|
141
|
+
- run: pnpm check
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Pin the action to an immutable release tag and let Dependabot propose upgrades.
|
|
145
|
+
|
|
146
|
+
## License and security
|
|
147
|
+
|
|
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).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plugins": ["typescript", "unicorn", "oxc", "react", "jsx-a11y", "import", "vitest"],
|
|
3
|
+
"categories": {
|
|
4
|
+
"correctness": "error",
|
|
5
|
+
"suspicious": "warn",
|
|
6
|
+
"perf": "warn"
|
|
7
|
+
},
|
|
8
|
+
"rules": {
|
|
9
|
+
"import/no-unassigned-import": "off",
|
|
10
|
+
"react/react-in-jsx-scope": "off",
|
|
11
|
+
"typescript/consistent-return": "off",
|
|
12
|
+
"typescript/no-unsafe-type-assertion": "off",
|
|
13
|
+
"unicorn/filename-case": "off",
|
|
14
|
+
"vitest/require-mock-type-parameters": "off"
|
|
15
|
+
},
|
|
16
|
+
"env": {
|
|
17
|
+
"builtin": true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"noUncheckedIndexedAccess": true,
|
|
5
|
+
"esModuleInterop": true,
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"moduleResolution": "Bundler",
|
|
9
|
+
"target": "ES2023",
|
|
10
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"verbatimModuleSyntax": true
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"noUncheckedIndexedAccess": true,
|
|
5
|
+
"exactOptionalPropertyTypes": true,
|
|
6
|
+
"module": "NodeNext",
|
|
7
|
+
"moduleResolution": "NodeNext",
|
|
8
|
+
"target": "ES2024",
|
|
9
|
+
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"verbatimModuleSyntax": true
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type OriginOptions = {
|
|
2
|
+
configured?: readonly (string | undefined)[];
|
|
3
|
+
allowReferer?: boolean;
|
|
4
|
+
trustForwardedHeaders?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function parseOrigin(value: string | undefined): string | undefined;
|
|
7
|
+
export declare function forwardedOrigin(request: Request): string | undefined;
|
|
8
|
+
export declare function acceptedOrigins(request: Request, options?: OriginOptions): string[];
|
|
9
|
+
export declare function validSameOriginRequest(request: Request, options?: OriginOptions): boolean;
|
|
10
|
+
export declare function requireSameOrigin(request: Request, options?: OriginOptions): void;
|
|
11
|
+
export declare function trustedOrigins(options?: OriginOptions): (request?: Request) => string[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function parseOrigin(value) {
|
|
2
|
+
if (!value?.trim())
|
|
3
|
+
return undefined;
|
|
4
|
+
try {
|
|
5
|
+
return new URL(value).origin;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function forwardedOrigin(request) {
|
|
12
|
+
const host = request.headers.get('x-forwarded-host')?.split(',')[0]?.trim() || request.headers.get('host')?.trim();
|
|
13
|
+
const protocol = request.headers.get('x-forwarded-proto')?.split(',')[0]?.trim();
|
|
14
|
+
return host && (protocol === 'http' || protocol === 'https') ? parseOrigin(`${protocol}://${host}`) : undefined;
|
|
15
|
+
}
|
|
16
|
+
export function acceptedOrigins(request, options = {}) {
|
|
17
|
+
return [
|
|
18
|
+
new URL(request.url).origin,
|
|
19
|
+
...(options.trustForwardedHeaders ? [forwardedOrigin(request)] : []),
|
|
20
|
+
...(options.configured ?? []).map(parseOrigin),
|
|
21
|
+
].filter((origin) => Boolean(origin));
|
|
22
|
+
}
|
|
23
|
+
export function validSameOriginRequest(request, options = {}) {
|
|
24
|
+
const origin = request.headers.get('origin') || (options.allowReferer ? parseOrigin(request.headers.get('referer') ?? undefined) : undefined);
|
|
25
|
+
const site = request.headers.get('sec-fetch-site');
|
|
26
|
+
return Boolean(origin && (!site || site === 'same-origin') && acceptedOrigins(request, options).includes(origin));
|
|
27
|
+
}
|
|
28
|
+
export function requireSameOrigin(request, options = {}) {
|
|
29
|
+
if (!validSameOriginRequest(request, options))
|
|
30
|
+
throw new Response('cross-origin mutation rejected', { status: 403 });
|
|
31
|
+
}
|
|
32
|
+
export function trustedOrigins(options = {}) {
|
|
33
|
+
return (request) => request ? acceptedOrigins(request, options).filter((origin) => origin !== new URL(request.url).origin) : [];
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=origins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"origins.js","sourceRoot":"","sources":["../../src/auth/origins.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,WAAW,CAAC,KAAyB;IACnD,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE;QAAE,OAAO,SAAS,CAAA;IACpC,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAgB;IAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAA;IAClH,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAA;IAChF,OAAO,IAAI,IAAI,CAAC,QAAQ,KAAK,MAAM,IAAI,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,QAAQ,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AACjH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAgB,EAAE,OAAO,GAAkB,EAAE;IAC3E,OAAO;QACL,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM;QAC3B,GAAG,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,GAAG,CAAC,OAAO,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC;KAC/C,CAAC,MAAM,CAAC,CAAC,MAAM,EAAoB,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;AACzD,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAgB,EAAE,OAAO,GAAkB,EAAE;IAClF,MAAM,MAAM,GACV,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;IAChI,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;IAClD,OAAO,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,IAAI,IAAI,KAAK,aAAa,CAAC,IAAI,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;AACnH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAgB,EAAE,OAAO,GAAkB,EAAE;IAC7E,IAAI,CAAC,sBAAsB,CAAC,OAAO,EAAE,OAAO,CAAC;QAAE,MAAM,IAAI,QAAQ,CAAC,gCAAgC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;AACtH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,OAAO,GAAkB,EAAE;IACxD,OAAO,CAAC,OAAiB,EAAE,EAAE,CAC3B,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;AAC/G,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ProviderCredentials = {
|
|
2
|
+
clientId: string;
|
|
3
|
+
clientSecret: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function configuredProviders<const Provider extends string>(providers: readonly Provider[], environment?: NodeJS.ProcessEnv): Provider[];
|
|
6
|
+
export declare function providerCredentials(provider: string, environment?: NodeJS.ProcessEnv): ProviderCredentials | undefined;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function configuredProviders(providers, environment = process.env) {
|
|
2
|
+
return providers.filter((provider) => {
|
|
3
|
+
const prefix = provider.toUpperCase().replaceAll('-', '_');
|
|
4
|
+
return Boolean(environment[`${prefix}_CLIENT_ID`]?.trim() && environment[`${prefix}_CLIENT_SECRET`]?.trim());
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
export function providerCredentials(provider, environment = process.env) {
|
|
8
|
+
const prefix = provider.toUpperCase().replaceAll('-', '_');
|
|
9
|
+
const clientId = environment[`${prefix}_CLIENT_ID`]?.trim();
|
|
10
|
+
const clientSecret = environment[`${prefix}_CLIENT_SECRET`]?.trim();
|
|
11
|
+
return clientId && clientSecret ? { clientId, clientSecret } : undefined;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=providers.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
export function randomToken(bytes = 16) {
|
|
3
|
+
if (!Number.isSafeInteger(bytes) || bytes < 1)
|
|
4
|
+
throw new RangeError('Token bytes must be a positive integer');
|
|
5
|
+
return crypto.randomBytes(bytes).toString('base64url');
|
|
6
|
+
}
|
|
7
|
+
export function randomId(bytes = 8) {
|
|
8
|
+
return randomToken(bytes);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=random.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"random.js","sourceRoot":"","sources":["../../src/auth/random.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAA;AAEhC,MAAM,UAAU,WAAW,CAAC,KAAK,GAAG,EAAE;IACpC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,wCAAwC,CAAC,CAAA;IAC7G,OAAO,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;AACxD,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAK,GAAG,CAAC;IAChC,OAAO,WAAW,CAAC,KAAK,CAAC,CAAA;AAC3B,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export function persistedSecret(options) {
|
|
5
|
+
const environment = options.environment ?? process.env;
|
|
6
|
+
const configured = environment[options.environmentKey ?? 'AUTH_SECRET']?.trim();
|
|
7
|
+
if (configured)
|
|
8
|
+
return configured;
|
|
9
|
+
const file = path.join(options.directory, options.filename ?? 'auth.secret');
|
|
10
|
+
try {
|
|
11
|
+
return readSecret(file);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (!isErrnoException(error) || error.code !== 'ENOENT')
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
fs.mkdirSync(options.directory, { recursive: true });
|
|
18
|
+
const secret = crypto.randomBytes(options.bytes ?? 32).toString('base64url');
|
|
19
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
20
|
+
try {
|
|
21
|
+
fs.writeFileSync(temporary, secret, { mode: 0o600, flag: 'wx' });
|
|
22
|
+
try {
|
|
23
|
+
fs.linkSync(temporary, file);
|
|
24
|
+
return secret;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (!isErrnoException(error) || error.code !== 'EEXIST')
|
|
28
|
+
throw error;
|
|
29
|
+
return readSecret(file);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
fs.rmSync(temporary, { force: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function readSecret(file) {
|
|
37
|
+
const secret = fs.readFileSync(file, 'utf8').trim();
|
|
38
|
+
if (!secret)
|
|
39
|
+
throw new Error(`Secret file is empty: ${file}`);
|
|
40
|
+
return secret;
|
|
41
|
+
}
|
|
42
|
+
function isErrnoException(error) {
|
|
43
|
+
return error instanceof Error && 'code' in error;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=secret.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret.js","sourceRoot":"","sources":["../../src/auth/secret.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAA;AAChC,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAU5B,MAAM,UAAU,eAAe,CAAC,OAA+B;IAC7D,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAA;IACtD,MAAM,UAAU,GAAG,WAAW,CAAC,OAAO,CAAC,cAAc,IAAI,aAAa,CAAC,EAAE,IAAI,EAAE,CAAA;IAC/E,IAAI,UAAU;QAAE,OAAO,UAAU,CAAA;IAEjC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,QAAQ,IAAI,aAAa,CAAC,CAAA;IAC5E,IAAI,CAAC;QACH,OAAO,UAAU,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;YAAE,MAAM,KAAK,CAAA;IACtE,CAAC;IAED,EAAE,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACpD,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;IAC5E,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,MAAM,CAAC,UAAU,EAAE,MAAM,CAAA;IACrE,IAAI,CAAC;QACH,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QAChE,IAAI,CAAC;YACH,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC,CAAA;YAC5B,OAAO,MAAM,CAAA;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ;gBAAE,MAAM,KAAK,CAAA;YACpE,OAAO,UAAU,CAAC,IAAI,CAAC,CAAA;QACzB,CAAC;IACH,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;IACvC,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAA;IACnD,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,EAAE,CAAC,CAAA;IAC7D,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAc;IACtC,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,CAAA;AAClD,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type RateLimitRule = {
|
|
2
|
+
window: number;
|
|
3
|
+
max: number;
|
|
4
|
+
};
|
|
5
|
+
export declare function standardSessionOptions(): {
|
|
6
|
+
expiresIn: number;
|
|
7
|
+
updateAge: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function standardRateLimitOptions(customRules?: Record<string, RateLimitRule>): {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
storage: 'database';
|
|
12
|
+
window: number;
|
|
13
|
+
max: number;
|
|
14
|
+
customRules: {
|
|
15
|
+
'/sign-in/email': {
|
|
16
|
+
window: number;
|
|
17
|
+
max: number;
|
|
18
|
+
};
|
|
19
|
+
'/sign-up/email': {
|
|
20
|
+
window: number;
|
|
21
|
+
max: number;
|
|
22
|
+
};
|
|
23
|
+
'/forget-password': {
|
|
24
|
+
window: number;
|
|
25
|
+
max: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function standardSessionOptions() {
|
|
2
|
+
return { expiresIn: 60 * 60 * 24 * 90, updateAge: 60 * 60 * 24 };
|
|
3
|
+
}
|
|
4
|
+
export function standardRateLimitOptions(customRules = {}) {
|
|
5
|
+
return {
|
|
6
|
+
enabled: true,
|
|
7
|
+
storage: 'database',
|
|
8
|
+
window: 60,
|
|
9
|
+
max: 120,
|
|
10
|
+
customRules: {
|
|
11
|
+
'/sign-in/email': { window: 60, max: 20 },
|
|
12
|
+
'/sign-up/email': { window: 60, max: 15 },
|
|
13
|
+
'/forget-password': { window: 60, max: 5 },
|
|
14
|
+
...customRules,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/auth/settings.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,sBAAsB;IACpC,OAAO,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAA;AAClE,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,WAAW,GAAkC,EAAE;IACtF,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,UAAmB;QAC5B,MAAM,EAAE,EAAE;QACV,GAAG,EAAE,GAAG;QACR,WAAW,EAAE;YACX,gBAAgB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACzC,gBAAgB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;YACzC,kBAAkB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE;YAC1C,GAAG,WAAW;SACf;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
export type EmailMessage = {
|
|
3
|
+
to: string;
|
|
4
|
+
subject: string;
|
|
5
|
+
text: string;
|
|
6
|
+
html?: string;
|
|
7
|
+
};
|
|
8
|
+
export type SmtpConfig = {
|
|
9
|
+
from: string;
|
|
10
|
+
host: string;
|
|
11
|
+
port: number;
|
|
12
|
+
secure: boolean;
|
|
13
|
+
user?: string;
|
|
14
|
+
password?: string;
|
|
15
|
+
};
|
|
16
|
+
export type SmtpEnvironmentOptions = {
|
|
17
|
+
host?: string;
|
|
18
|
+
port?: string;
|
|
19
|
+
secure?: string;
|
|
20
|
+
user?: string;
|
|
21
|
+
password?: string;
|
|
22
|
+
from?: string;
|
|
23
|
+
};
|
|
24
|
+
export type EmailDelivery = {
|
|
25
|
+
send(message: EmailMessage): Promise<void>;
|
|
26
|
+
verify(): Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
export declare function smtpConfigFromEnvironment(environment?: NodeJS.ProcessEnv, keys?: SmtpEnvironmentOptions): SmtpConfig | undefined;
|
|
29
|
+
export declare function createSmtpDelivery(config: SmtpConfig): EmailDelivery;
|
|
30
|
+
export declare function createSmtpTransport(config: SmtpConfig): nodemailer.Transporter<import("nodemailer/lib/smtp-transport/index.js").SentMessageInfo, import("nodemailer/lib/smtp-transport/index.js").Options>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
export function smtpConfigFromEnvironment(environment = process.env, keys = {}) {
|
|
3
|
+
const host = environment[keys.host ?? 'SMTP_HOST']?.trim();
|
|
4
|
+
const from = environment[keys.from ?? 'EMAIL_FROM']?.trim();
|
|
5
|
+
if (!host && !from)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (!host)
|
|
8
|
+
throw new Error(`${keys.host ?? 'SMTP_HOST'} is required for SMTP email`);
|
|
9
|
+
if (!from)
|
|
10
|
+
throw new Error(`${keys.from ?? 'EMAIL_FROM'} is required for SMTP email`);
|
|
11
|
+
const portKey = keys.port ?? 'SMTP_PORT';
|
|
12
|
+
const port = Number(environment[portKey] ?? 587);
|
|
13
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535)
|
|
14
|
+
throw new Error(`${portKey} must be a valid TCP port`);
|
|
15
|
+
const userKey = keys.user ?? 'SMTP_USER';
|
|
16
|
+
const passwordKey = keys.password ?? 'SMTP_PASSWORD';
|
|
17
|
+
const user = environment[userKey]?.trim();
|
|
18
|
+
const password = environment[passwordKey];
|
|
19
|
+
if (Boolean(user) !== Boolean(password))
|
|
20
|
+
throw new Error(`${userKey} and ${passwordKey} must be configured together`);
|
|
21
|
+
return {
|
|
22
|
+
from,
|
|
23
|
+
host,
|
|
24
|
+
port,
|
|
25
|
+
secure: environment[keys.secure ?? 'SMTP_SECURE']?.trim().toLowerCase() === 'true',
|
|
26
|
+
...(user && password ? { user, password } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function createSmtpDelivery(config) {
|
|
30
|
+
const transport = createSmtpTransport(config);
|
|
31
|
+
return {
|
|
32
|
+
send: async (message) => transport.sendMail({ from: config.from, ...message }).then(() => undefined),
|
|
33
|
+
verify: async () => transport.verify().then(() => undefined),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function createSmtpTransport(config) {
|
|
37
|
+
return nodemailer.createTransport({
|
|
38
|
+
host: config.host,
|
|
39
|
+
port: config.port,
|
|
40
|
+
secure: config.secure,
|
|
41
|
+
auth: config.user ? { user: config.user, pass: config.password } : undefined,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/email/index.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAA;AA2BnC,MAAM,UAAU,yBAAyB,CACvC,WAAW,GAAsB,OAAO,CAAC,GAAG,EAC5C,IAAI,GAA2B,EAAE;IAEjC,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,IAAI,WAAW,CAAC,EAAE,IAAI,EAAE,CAAA;IAC1D,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,IAAI,YAAY,CAAC,EAAE,IAAI,EAAE,CAAA;IAC3D,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAA;IACpC,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,WAAW,6BAA6B,CAAC,CAAA;IACpF,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,YAAY,6BAA6B,CAAC,CAAA;IAErF,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,IAAI,WAAW,CAAA;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,CAAA;IAChD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,2BAA2B,CAAC,CAAA;IAEhH,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,IAAI,WAAW,CAAA;IACxC,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,IAAI,eAAe,CAAA;IACpD,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAA;IACzC,MAAM,QAAQ,GAAG,WAAW,CAAC,WAAW,CAAC,CAAA;IACzC,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,QAAQ,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,QAAQ,WAAW,8BAA8B,CAAC,CAAA;IAErH,OAAO;QACL,IAAI;QACJ,IAAI;QACJ,IAAI;QACJ,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,MAAM,IAAI,aAAa,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM;QAClF,GAAG,CAAC,IAAI,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChD,CAAA;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAkB;IACnD,MAAM,SAAS,GAAG,mBAAmB,CAAC,MAAM,CAAC,CAAA;IAC7C,OAAO;QACL,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;QACpG,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;KAC7D,CAAA;AACH,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAkB;IACpD,OAAO,UAAU,CAAC,eAAe,CAAC;QAChC,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,SAAS;KAC7E,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/realtime/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type CentrifugoPublisherOptions = {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
fetch?: typeof fetch;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
retryMs?: number;
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
maxConcurrentChannels?: number;
|
|
9
|
+
maxPendingChannels?: number;
|
|
10
|
+
onError: (error: unknown, channel: string) => void;
|
|
11
|
+
onRetry?: (error: unknown, channel: string) => void;
|
|
12
|
+
};
|
|
13
|
+
export declare class CentrifugoPublisher {
|
|
14
|
+
private readonly options;
|
|
15
|
+
private readonly pending;
|
|
16
|
+
private readonly queue;
|
|
17
|
+
private readonly idleWaiters;
|
|
18
|
+
private readonly request;
|
|
19
|
+
private readonly maxConcurrentChannels;
|
|
20
|
+
private readonly maxPendingChannels;
|
|
21
|
+
private active;
|
|
22
|
+
private closed;
|
|
23
|
+
constructor(options: CentrifugoPublisherOptions);
|
|
24
|
+
publish(channel: string, data: unknown): boolean;
|
|
25
|
+
idle(): Promise<void>;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
private pump;
|
|
28
|
+
private flush;
|
|
29
|
+
private deliver;
|
|
30
|
+
private deliverOnce;
|
|
31
|
+
private reportError;
|
|
32
|
+
private isIdle;
|
|
33
|
+
private resolveIdle;
|
|
34
|
+
}
|