@octanejs/app-core 0.0.9 → 0.0.12
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/package.json +4 -4
- package/src/constants.js +2 -0
- package/src/index.js +1 -0
- package/src/resolve-config.js +50 -1
- package/src/server/production.js +9 -18
- package/src/server/rpc.js +254 -0
- package/src/server/server-entry.js +2 -0
- package/types/index.d.ts +29 -0
- package/types/production.d.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/app-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -75,14 +75,14 @@
|
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@ripple-ts/adapter": "^0.3.
|
|
78
|
+
"@ripple-ts/adapter": "^0.3.112",
|
|
79
79
|
"esbuild": "^0.28.1"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
|
-
"octane": "0.1.
|
|
82
|
+
"octane": "0.1.16"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/node": "^24.13.3",
|
|
86
|
-
"octane": "0.1.
|
|
86
|
+
"octane": "0.1.16"
|
|
87
87
|
}
|
|
88
88
|
}
|
package/src/constants.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
export const DEFAULT_OUTDIR = 'dist';
|
|
3
3
|
export const ENTRY_FILENAME = 'entry.js';
|
|
4
|
+
/** Default upper bound for an encoded server-function request body. */
|
|
5
|
+
export const DEFAULT_RPC_MAX_BODY_BYTES = 1_048_576;
|
|
4
6
|
/** Context.state key a middleware sets to apply a per-request CSP nonce. */
|
|
5
7
|
export const OCTANE_NONCE_STATE_KEY = 'octane.nonce';
|
package/src/index.js
CHANGED
package/src/resolve-config.js
CHANGED
|
@@ -15,7 +15,34 @@
|
|
|
15
15
|
|
|
16
16
|
import { normalizeRendererConfig } from 'octane/compiler/renderers';
|
|
17
17
|
|
|
18
|
-
import { DEFAULT_OUTDIR } from './constants.js';
|
|
18
|
+
import { DEFAULT_OUTDIR, DEFAULT_RPC_MAX_BODY_BYTES } from './constants.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {unknown} value
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
function normalize_rpc_origin(value) {
|
|
25
|
+
if (typeof value !== 'string') {
|
|
26
|
+
throw new Error('[octane] server.rpc.allowedOrigins must contain only HTTP or HTTPS origins.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const origin = new URL(value);
|
|
31
|
+
if (
|
|
32
|
+
(origin.protocol !== 'http:' && origin.protocol !== 'https:') ||
|
|
33
|
+
origin.username !== '' ||
|
|
34
|
+
origin.password !== '' ||
|
|
35
|
+
origin.pathname !== '/' ||
|
|
36
|
+
origin.search !== '' ||
|
|
37
|
+
origin.hash !== ''
|
|
38
|
+
) {
|
|
39
|
+
throw new Error('Not an HTTP or HTTPS origin');
|
|
40
|
+
}
|
|
41
|
+
return origin.origin;
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error('[octane] server.rpc.allowedOrigins must contain only HTTP or HTTPS origins.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
19
46
|
|
|
20
47
|
/**
|
|
21
48
|
* @param {unknown} route
|
|
@@ -176,6 +203,24 @@ export function resolveOctaneConfig(raw, options = {}) {
|
|
|
176
203
|
throw new Error("[octane] server.render must be 'streaming' or 'buffered'.");
|
|
177
204
|
}
|
|
178
205
|
|
|
206
|
+
const rawRpc = raw.server?.rpc;
|
|
207
|
+
if (
|
|
208
|
+
rawRpc !== undefined &&
|
|
209
|
+
(rawRpc === null || typeof rawRpc !== 'object' || Array.isArray(rawRpc))
|
|
210
|
+
) {
|
|
211
|
+
throw new Error('[octane] server.rpc must be an object when provided.');
|
|
212
|
+
}
|
|
213
|
+
if (
|
|
214
|
+
rawRpc?.maxBodyBytes !== undefined &&
|
|
215
|
+
(!Number.isSafeInteger(rawRpc.maxBodyBytes) || rawRpc.maxBodyBytes <= 0)
|
|
216
|
+
) {
|
|
217
|
+
throw new Error('[octane] server.rpc.maxBodyBytes must be a positive safe integer.');
|
|
218
|
+
}
|
|
219
|
+
if (rawRpc?.allowedOrigins !== undefined && !Array.isArray(rawRpc.allowedOrigins)) {
|
|
220
|
+
throw new Error('[octane] server.rpc.allowedOrigins must be an array.');
|
|
221
|
+
}
|
|
222
|
+
const allowedRpcOrigins = [...new Set((rawRpc?.allowedOrigins ?? []).map(normalize_rpc_origin))];
|
|
223
|
+
|
|
179
224
|
// ------------------------------------------------------------------
|
|
180
225
|
// Apply defaults
|
|
181
226
|
// ------------------------------------------------------------------
|
|
@@ -201,6 +246,10 @@ export function resolveOctaneConfig(raw, options = {}) {
|
|
|
201
246
|
server: {
|
|
202
247
|
trustProxy: raw.server?.trustProxy ?? false,
|
|
203
248
|
render: raw.server?.render ?? 'streaming',
|
|
249
|
+
rpc: {
|
|
250
|
+
allowedOrigins: allowedRpcOrigins,
|
|
251
|
+
maxBodyBytes: rawRpc?.maxBodyBytes ?? DEFAULT_RPC_MAX_BODY_BYTES,
|
|
252
|
+
},
|
|
204
253
|
},
|
|
205
254
|
};
|
|
206
255
|
}
|
package/src/server/production.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { createRouter } from './router.js';
|
|
26
26
|
import { createContext, runMiddlewareChain } from './middleware.js';
|
|
27
|
+
import { handleRpcRequest } from './rpc.js';
|
|
27
28
|
import { handleServerRoute } from './server-route.js';
|
|
28
29
|
import { composeHtmlStream } from './html-stream.js';
|
|
29
30
|
import {
|
|
@@ -48,12 +49,7 @@ import {
|
|
|
48
49
|
get_route_entry_export_name,
|
|
49
50
|
get_route_entry_path,
|
|
50
51
|
} from '../routes.js';
|
|
51
|
-
import {
|
|
52
|
-
patch_global_fetch,
|
|
53
|
-
build_rpc_lookup,
|
|
54
|
-
is_rpc_request,
|
|
55
|
-
handle_rpc_request,
|
|
56
|
-
} from '@ripple-ts/adapter/rpc';
|
|
52
|
+
import { patch_global_fetch, build_rpc_lookup, is_rpc_request } from '@ripple-ts/adapter/rpc';
|
|
57
53
|
|
|
58
54
|
export { resolveOctaneConfig } from '../resolve-config.js';
|
|
59
55
|
|
|
@@ -127,6 +123,7 @@ export function createHandler(manifest, deps) {
|
|
|
127
123
|
const router = createRouter(manifest.routes);
|
|
128
124
|
const globalMiddlewares = manifest.middlewares ?? [];
|
|
129
125
|
const trustProxy = manifest.trustProxy ?? false;
|
|
126
|
+
const rpcPolicy = manifest.rpc;
|
|
130
127
|
const runtime = manifest.runtime;
|
|
131
128
|
validateSsrTemplate(htmlTemplate);
|
|
132
129
|
// Also pin the built-template contract up front. The marker is emitted by
|
|
@@ -158,17 +155,7 @@ export function createHandler(manifest, deps) {
|
|
|
158
155
|
headers: { 'Content-Type': 'application/json' },
|
|
159
156
|
});
|
|
160
157
|
}
|
|
161
|
-
|
|
162
|
-
const requestAsyncContext =
|
|
163
|
-
platform === undefined
|
|
164
|
-
? asyncContext
|
|
165
|
-
: {
|
|
166
|
-
run(store, fn) {
|
|
167
|
-
return asyncContext.run({ ...store, platform }, fn);
|
|
168
|
-
},
|
|
169
|
-
getStore: () => asyncContext.getStore(),
|
|
170
|
-
};
|
|
171
|
-
return handle_rpc_request(request, {
|
|
158
|
+
return handleRpcRequest(request, {
|
|
172
159
|
resolveFunction(/** @type {string} */ hash) {
|
|
173
160
|
const entry = rpcLookup.get(hash);
|
|
174
161
|
if (!entry) return null;
|
|
@@ -176,8 +163,12 @@ export function createHandler(manifest, deps) {
|
|
|
176
163
|
return typeof fn === 'function' ? fn : null;
|
|
177
164
|
},
|
|
178
165
|
executeServerFunction,
|
|
179
|
-
asyncContext
|
|
166
|
+
asyncContext,
|
|
180
167
|
trustProxy,
|
|
168
|
+
middlewares: globalMiddlewares,
|
|
169
|
+
allowedOrigins: rpcPolicy?.allowedOrigins,
|
|
170
|
+
maxBodyBytes: rpcPolicy?.maxBodyBytes,
|
|
171
|
+
platform,
|
|
181
172
|
});
|
|
182
173
|
}
|
|
183
174
|
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Bundler-neutral, security-owning server-function request boundary.
|
|
4
|
+
*
|
|
5
|
+
* The Ripple adapter owns the existing wire path, hash lookup, proxy-origin
|
|
6
|
+
* derivation, and request-scoped fetch primitives. Octane owns which requests
|
|
7
|
+
* may cross its server-function boundary, the bounded body reader, application
|
|
8
|
+
* authorization middleware, and production-safe error disclosure.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { derive_origin } from '@ripple-ts/adapter/rpc';
|
|
12
|
+
|
|
13
|
+
import { DEFAULT_RPC_MAX_BODY_BYTES } from '../constants.js';
|
|
14
|
+
import { createContext, runMiddlewareChain } from './middleware.js';
|
|
15
|
+
|
|
16
|
+
const RPC_PATH_PREFIX = '/_$_ripple_rpc_$_/';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {number} status
|
|
20
|
+
* @param {string} message
|
|
21
|
+
* @param {HeadersInit} [headers]
|
|
22
|
+
* @returns {Response}
|
|
23
|
+
*/
|
|
24
|
+
function rpcError(status, message, headers) {
|
|
25
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
26
|
+
status,
|
|
27
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8', ...headers },
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {Request} request
|
|
33
|
+
* @param {boolean} trustProxy
|
|
34
|
+
* @param {readonly string[]} allowedOrigins
|
|
35
|
+
* @returns {string | null}
|
|
36
|
+
*/
|
|
37
|
+
function allowedRequestOrigin(request, trustProxy, allowedOrigins) {
|
|
38
|
+
let requestOrigin;
|
|
39
|
+
try {
|
|
40
|
+
requestOrigin = new URL(derive_origin(request, trustProxy)).origin;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const browserOrigin = request.headers.get('origin');
|
|
46
|
+
if (browserOrigin === null) {
|
|
47
|
+
return request.headers.get('sec-fetch-site')?.toLowerCase() === 'cross-site'
|
|
48
|
+
? null
|
|
49
|
+
: requestOrigin;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let normalizedBrowserOrigin;
|
|
53
|
+
try {
|
|
54
|
+
normalizedBrowserOrigin = new URL(browserOrigin).origin;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (normalizedBrowserOrigin !== browserOrigin) return null;
|
|
59
|
+
|
|
60
|
+
return normalizedBrowserOrigin === requestOrigin ||
|
|
61
|
+
allowedOrigins.includes(normalizedBrowserOrigin)
|
|
62
|
+
? requestOrigin
|
|
63
|
+
: null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {Response} response
|
|
68
|
+
* @param {string | null} origin
|
|
69
|
+
* @returns {Response}
|
|
70
|
+
*/
|
|
71
|
+
function withRpcCors(response, origin) {
|
|
72
|
+
if (origin === null) return response;
|
|
73
|
+
|
|
74
|
+
const headers = new Headers(response.headers);
|
|
75
|
+
headers.set('Access-Control-Allow-Origin', origin);
|
|
76
|
+
headers.append('Vary', 'Origin');
|
|
77
|
+
return new Response(response.body, {
|
|
78
|
+
status: response.status,
|
|
79
|
+
statusText: response.statusText,
|
|
80
|
+
headers,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {Request} request
|
|
86
|
+
* @param {number} maxBodyBytes
|
|
87
|
+
* @returns {Promise<{ body: string } | { error: Response }>}
|
|
88
|
+
*/
|
|
89
|
+
async function readBoundedRpcBody(request, maxBodyBytes) {
|
|
90
|
+
const contentLength = request.headers.get('content-length');
|
|
91
|
+
if (contentLength !== null) {
|
|
92
|
+
if (!/^\d+$/.test(contentLength)) {
|
|
93
|
+
return { error: rpcError(400, 'Invalid RPC request') };
|
|
94
|
+
}
|
|
95
|
+
const declaredLength = Number(contentLength);
|
|
96
|
+
if (!Number.isSafeInteger(declaredLength) || declaredLength > maxBodyBytes) {
|
|
97
|
+
return { error: rpcError(413, 'RPC request exceeds the maximum body size') };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (request.body === null || request.signal.aborted) {
|
|
102
|
+
return { error: rpcError(400, 'Invalid RPC request') };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const reader = request.body.getReader();
|
|
106
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
107
|
+
let bytesRead = 0;
|
|
108
|
+
let body = '';
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
while (true) {
|
|
112
|
+
const { done, value } = await reader.read();
|
|
113
|
+
if (done) break;
|
|
114
|
+
bytesRead += value.byteLength;
|
|
115
|
+
if (bytesRead > maxBodyBytes) {
|
|
116
|
+
try {
|
|
117
|
+
await reader.cancel();
|
|
118
|
+
} catch {
|
|
119
|
+
// A hostile stream cannot downgrade an oversized request into another status.
|
|
120
|
+
}
|
|
121
|
+
return { error: rpcError(413, 'RPC request exceeds the maximum body size') };
|
|
122
|
+
}
|
|
123
|
+
body += decoder.decode(value, { stream: true });
|
|
124
|
+
}
|
|
125
|
+
body += decoder.decode();
|
|
126
|
+
} catch {
|
|
127
|
+
return { error: rpcError(400, 'Invalid RPC request') };
|
|
128
|
+
} finally {
|
|
129
|
+
reader.releaseLock();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
if (!Array.isArray(JSON.parse(body))) {
|
|
134
|
+
return { error: rpcError(400, 'Invalid server function arguments') };
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
return { error: rpcError(400, 'Invalid server function arguments') };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { body };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {unknown} error
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
function isInvalidRpcPayload(error) {
|
|
148
|
+
return (
|
|
149
|
+
typeof error === 'object' &&
|
|
150
|
+
error !== null &&
|
|
151
|
+
'code' in error &&
|
|
152
|
+
error.code === 'OCTANE_INVALID_RPC_PAYLOAD'
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Validate and execute an Octane server function behind application middleware.
|
|
158
|
+
*
|
|
159
|
+
* @param {Request} request
|
|
160
|
+
* @param {import('@octanejs/app-core').RpcRequestOptions} options
|
|
161
|
+
* @returns {Promise<Response>}
|
|
162
|
+
*/
|
|
163
|
+
export async function handleRpcRequest(request, options) {
|
|
164
|
+
if (request.method === 'OPTIONS') {
|
|
165
|
+
const origin = allowedRequestOrigin(
|
|
166
|
+
request,
|
|
167
|
+
options.trustProxy ?? false,
|
|
168
|
+
options.allowedOrigins ?? [],
|
|
169
|
+
);
|
|
170
|
+
const browserOrigin = request.headers.get('origin');
|
|
171
|
+
if (origin === null || browserOrigin === null || browserOrigin === origin) {
|
|
172
|
+
return rpcError(403, 'Cross-origin RPC requests are not allowed');
|
|
173
|
+
}
|
|
174
|
+
if (request.headers.get('access-control-request-method') !== 'POST') {
|
|
175
|
+
return rpcError(405, 'RPC requests require POST', { Allow: 'POST' });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return new Response(null, {
|
|
179
|
+
status: 204,
|
|
180
|
+
headers: {
|
|
181
|
+
'Access-Control-Allow-Origin': browserOrigin,
|
|
182
|
+
'Access-Control-Allow-Methods': 'POST',
|
|
183
|
+
'Access-Control-Allow-Headers':
|
|
184
|
+
request.headers.get('access-control-request-headers') ?? 'content-type',
|
|
185
|
+
Vary: 'Origin, Access-Control-Request-Headers',
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (request.method !== 'POST') {
|
|
191
|
+
return rpcError(405, 'RPC requests require POST', { Allow: 'POST' });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const contentType = request.headers.get('content-type');
|
|
195
|
+
if (contentType === null || !/^application\/json(?:\s*;|\s*$)/i.test(contentType)) {
|
|
196
|
+
return rpcError(415, 'RPC requests require application/json');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const hash = new URL(request.url).pathname.slice(RPC_PATH_PREFIX.length);
|
|
200
|
+
if (!/^[a-f0-9]{8}$/.test(hash)) {
|
|
201
|
+
return rpcError(400, 'Invalid RPC request');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const origin = allowedRequestOrigin(
|
|
205
|
+
request,
|
|
206
|
+
options.trustProxy ?? false,
|
|
207
|
+
options.allowedOrigins ?? [],
|
|
208
|
+
);
|
|
209
|
+
if (origin === null) {
|
|
210
|
+
return rpcError(403, 'Cross-origin RPC requests are not allowed');
|
|
211
|
+
}
|
|
212
|
+
const browserOrigin = request.headers.get('origin');
|
|
213
|
+
const corsOrigin = browserOrigin === origin ? null : browserOrigin;
|
|
214
|
+
|
|
215
|
+
const context = createContext(request, {}, options.platform);
|
|
216
|
+
const store =
|
|
217
|
+
options.platform === undefined ? { origin } : { origin, platform: options.platform };
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const response = await options.asyncContext.run(store, async () =>
|
|
221
|
+
runMiddlewareChain(
|
|
222
|
+
context,
|
|
223
|
+
options.middlewares ?? [],
|
|
224
|
+
[],
|
|
225
|
+
async () => {
|
|
226
|
+
const fn = await options.resolveFunction(hash);
|
|
227
|
+
if (fn === null) return rpcError(404, 'RPC function not found');
|
|
228
|
+
const payload = await readBoundedRpcBody(
|
|
229
|
+
request,
|
|
230
|
+
options.maxBodyBytes ?? DEFAULT_RPC_MAX_BODY_BYTES,
|
|
231
|
+
);
|
|
232
|
+
if ('error' in payload) return payload.error;
|
|
233
|
+
try {
|
|
234
|
+
const result = await options.executeServerFunction(fn, payload.body);
|
|
235
|
+
return new Response(result, {
|
|
236
|
+
status: 200,
|
|
237
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (isInvalidRpcPayload(error)) {
|
|
241
|
+
return rpcError(400, 'Invalid server function arguments');
|
|
242
|
+
}
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
[],
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
return withRpcCors(response, corsOrigin);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error('[octane] RPC request error:', error);
|
|
252
|
+
return withRpcCors(rpcError(500, 'Internal Server Error'), corsOrigin);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -277,6 +277,7 @@ export const manifest = {
|
|
|
277
277
|
layouts,
|
|
278
278
|
middlewares: octaneConfig.middlewares,
|
|
279
279
|
trustProxy: octaneConfig.server.trustProxy,
|
|
280
|
+
rpc: octaneConfig.server.rpc,
|
|
280
281
|
render: octaneConfig.server.render,
|
|
281
282
|
rootBoundary,
|
|
282
283
|
rootBoundaryEntries,
|
|
@@ -387,6 +388,7 @@ export const handler = createHandler(
|
|
|
387
388
|
layouts,
|
|
388
389
|
middlewares: octaneConfig.middlewares,
|
|
389
390
|
trustProxy: octaneConfig.server.trustProxy,
|
|
391
|
+
rpc: octaneConfig.server.rpc,
|
|
390
392
|
render: octaneConfig.server.render,
|
|
391
393
|
rootBoundary,
|
|
392
394
|
rootBoundaryEntries,
|
package/types/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RuntimePrimitives } from '@ripple-ts/adapter';
|
|
2
|
+
import type { AsyncContext } from '@ripple-ts/adapter/rpc';
|
|
2
3
|
|
|
3
4
|
// ============================================================================
|
|
4
5
|
// Shared app/config exports
|
|
@@ -138,6 +139,21 @@ export function handleServerRoute(
|
|
|
138
139
|
): Promise<Response>;
|
|
139
140
|
export function is_rpc_request(pathname: string): boolean;
|
|
140
141
|
|
|
142
|
+
/** Security policy and execution dependencies for a server-function request. */
|
|
143
|
+
export interface RpcRequestOptions {
|
|
144
|
+
resolveFunction: (hash: string) => Function | null | Promise<Function | null>;
|
|
145
|
+
executeServerFunction: (fn: Function, body: string) => Promise<string>;
|
|
146
|
+
asyncContext: AsyncContext<{ origin?: string; platform?: unknown }>;
|
|
147
|
+
trustProxy?: boolean;
|
|
148
|
+
middlewares?: Middleware[];
|
|
149
|
+
allowedOrigins?: readonly string[];
|
|
150
|
+
maxBodyBytes?: number;
|
|
151
|
+
platform?: unknown;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Apply Octane's security policy and global middleware to a server function. */
|
|
155
|
+
export function handleRpcRequest(request: Request, options: RpcRequestOptions): Promise<Response>;
|
|
156
|
+
|
|
141
157
|
// ============================================================================
|
|
142
158
|
// Configuration
|
|
143
159
|
// ============================================================================
|
|
@@ -339,6 +355,13 @@ export interface OctaneConfigOptions {
|
|
|
339
355
|
* @default false
|
|
340
356
|
*/
|
|
341
357
|
trustProxy?: boolean;
|
|
358
|
+
/** Security policy for compiler-generated `module server` requests. */
|
|
359
|
+
rpc?: {
|
|
360
|
+
/** Additional exact HTTP(S) origins permitted to call server functions. */
|
|
361
|
+
allowedOrigins?: string[];
|
|
362
|
+
/** Maximum encoded request size in bytes. @default 1048576 */
|
|
363
|
+
maxBodyBytes?: number;
|
|
364
|
+
};
|
|
342
365
|
/**
|
|
343
366
|
* Production SSR mode: 'streaming' (default) flushes the shell at
|
|
344
367
|
* first await and streams suspense segments out-of-order (same engine
|
|
@@ -378,6 +401,12 @@ export interface ResolvedOctaneConfig {
|
|
|
378
401
|
server: {
|
|
379
402
|
/** @default false */
|
|
380
403
|
trustProxy: boolean;
|
|
404
|
+
rpc: {
|
|
405
|
+
/** Additional normalized HTTP(S) origins. @default [] */
|
|
406
|
+
allowedOrigins: string[];
|
|
407
|
+
/** Maximum encoded request size in bytes. @default 1048576 */
|
|
408
|
+
maxBodyBytes: number;
|
|
409
|
+
};
|
|
381
410
|
/** @default 'streaming' */
|
|
382
411
|
render: 'streaming' | 'buffered';
|
|
383
412
|
};
|
package/types/production.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface ServerManifest {
|
|
|
29
29
|
middlewares: Middleware[];
|
|
30
30
|
/** Trust X-Forwarded-* headers when deriving origin for RPC fetch */
|
|
31
31
|
trustProxy?: boolean;
|
|
32
|
+
/** Validated `module server` origin and body-size policy. */
|
|
33
|
+
rpc?: Partial<ResolvedOctaneConfig['server']['rpc']>;
|
|
32
34
|
/** 'streaming' (default) renders via renderToReadableStream; 'buffered' awaits everything via prerender */
|
|
33
35
|
render?: 'streaming' | 'buffered';
|
|
34
36
|
/** Resolved server-compiled global boundary components. */
|