@ripple-ts/adapter 0.2.213 → 0.2.214
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/CHANGELOG.md +2 -0
- package/package.json +7 -2
- package/src/index.js +2 -88
- package/src/rpc.js +184 -0
- package/tests/index.test.js +0 -98
- package/types/index.d.ts +3 -5
- package/types/rpc.d.ts +93 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Shared adapter primitives for Ripple metaframework adapters",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Dominic Gannaway",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.214",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"module": "src/index.js",
|
|
9
9
|
"main": "src/index.js",
|
|
@@ -12,9 +12,14 @@
|
|
|
12
12
|
"types": "./types/index.d.ts",
|
|
13
13
|
"import": "./src/index.js",
|
|
14
14
|
"default": "./src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./rpc": {
|
|
17
|
+
"types": "./types/rpc.d.ts",
|
|
18
|
+
"import": "./src/rpc.js",
|
|
19
|
+
"default": "./src/rpc.js"
|
|
15
20
|
}
|
|
16
21
|
},
|
|
17
|
-
"homepage": "https://
|
|
22
|
+
"homepage": "https://ripple-ts.com",
|
|
18
23
|
"repository": {
|
|
19
24
|
"type": "git",
|
|
20
25
|
"url": "git+https://github.com/Ripple-TS/ripple.git",
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
// NOTE: For now we'd not have it fully runtime-agnostic
|
|
2
|
-
// When we'd like to move away from Node.js modules or explore other runtime options.
|
|
3
|
-
import { createReadStream, existsSync, statSync } from 'node:fs';
|
|
4
|
-
import { extname, resolve, sep } from 'node:path';
|
|
5
|
-
import { Readable } from 'node:stream';
|
|
6
|
-
|
|
7
1
|
export const DEFAULT_HOSTNAME = 'localhost';
|
|
8
2
|
export const DEFAULT_PORT = 3000;
|
|
9
3
|
export const DEFAULT_STATIC_PREFIX = '/';
|
|
@@ -116,87 +110,7 @@ export function get_static_cache_control(
|
|
|
116
110
|
* @returns {string}
|
|
117
111
|
*/
|
|
118
112
|
export function get_mime_type(pathname) {
|
|
119
|
-
const
|
|
113
|
+
const dot = pathname.lastIndexOf('.');
|
|
114
|
+
const extension = dot !== -1 ? pathname.slice(dot).toLowerCase() : '';
|
|
120
115
|
return MIME_TYPES[extension] || 'application/octet-stream';
|
|
121
116
|
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* @param {string} base_dir
|
|
125
|
-
* @param {string} pathname
|
|
126
|
-
* @returns {string | null}
|
|
127
|
-
*/
|
|
128
|
-
function resolve_static_file_path(base_dir, pathname) {
|
|
129
|
-
const file_path = resolve(base_dir, `.${pathname}`);
|
|
130
|
-
const is_within_base_dir = file_path === base_dir || file_path.startsWith(base_dir + sep);
|
|
131
|
-
return is_within_base_dir ? file_path : null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Create a request-based static file handler that can be used by adapters.
|
|
136
|
-
*
|
|
137
|
-
* @param {string} dir - Directory to serve files from (relative to cwd or absolute)
|
|
138
|
-
* @param {{ prefix?: string, maxAge?: number, immutable?: boolean }} [options]
|
|
139
|
-
* @returns {(request: Request) => Response | null}
|
|
140
|
-
*/
|
|
141
|
-
export function serveStatic(dir, options = {}) {
|
|
142
|
-
const {
|
|
143
|
-
prefix = DEFAULT_STATIC_PREFIX,
|
|
144
|
-
maxAge = DEFAULT_STATIC_MAX_AGE,
|
|
145
|
-
immutable = false,
|
|
146
|
-
} = options;
|
|
147
|
-
|
|
148
|
-
const base_dir = resolve(dir);
|
|
149
|
-
|
|
150
|
-
return function serve_static_request(request) {
|
|
151
|
-
const request_method = (request.method || 'GET').toUpperCase();
|
|
152
|
-
if (request_method !== 'GET' && request_method !== 'HEAD') {
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let pathname;
|
|
157
|
-
try {
|
|
158
|
-
pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname);
|
|
159
|
-
} catch {
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (!pathname.startsWith(prefix)) {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
pathname = pathname.slice(prefix.length) || '/';
|
|
168
|
-
if (!pathname.startsWith('/')) {
|
|
169
|
-
pathname = '/' + pathname;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const file_path = resolve_static_file_path(base_dir, pathname);
|
|
173
|
-
if (file_path === null || !existsSync(file_path)) {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
let file_stats;
|
|
178
|
-
try {
|
|
179
|
-
file_stats = statSync(file_path);
|
|
180
|
-
} catch {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (file_stats.isDirectory()) {
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const headers = new Headers();
|
|
189
|
-
headers.set('Content-Type', get_mime_type(file_path));
|
|
190
|
-
headers.set('Content-Length', String(file_stats.size));
|
|
191
|
-
headers.set('Cache-Control', get_static_cache_control(pathname, maxAge, immutable));
|
|
192
|
-
|
|
193
|
-
if (request_method === 'HEAD') {
|
|
194
|
-
return new Response(null, { status: 200, headers });
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// `Readable.toWeb()` can be typed differently across Node/Bun/libdom contexts.
|
|
198
|
-
/** @type {BodyInit} */
|
|
199
|
-
const file_body = /** @type {any} */ (Readable.toWeb(createReadStream(file_path)));
|
|
200
|
-
return new Response(file_body, { status: 200, headers });
|
|
201
|
-
};
|
|
202
|
-
}
|
package/src/rpc.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared RPC utilities for Ripple metaframework.
|
|
3
|
+
*
|
|
4
|
+
* These functions are platform-agnostic — they use only standard Web APIs
|
|
5
|
+
* (Request, Response, Headers, URL) and receive platform-specific capabilities
|
|
6
|
+
* (hashing, async context) from the adapter's runtime.
|
|
7
|
+
*
|
|
8
|
+
* Used by both the Vite dev server and production server runtime.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
@import {
|
|
13
|
+
RipplePatchedFetch,
|
|
14
|
+
RpcEntry,
|
|
15
|
+
handle_rpc_request,
|
|
16
|
+
is_rpc_request,
|
|
17
|
+
build_rpc_lookup,
|
|
18
|
+
patch_global_fetch,
|
|
19
|
+
} from '@ripple-ts/adapter/rpc';
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const RPC_PATH_PREFIX = '/_$_ripple_rpc_$_/';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Origin derivation
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/** @type {import('@ripple-ts/adapter/rpc').derive_origin} */
|
|
29
|
+
export function derive_origin(request, trust_proxy) {
|
|
30
|
+
const url = new URL(request.url);
|
|
31
|
+
let protocol = url.protocol.replace(':', '');
|
|
32
|
+
let host = url.host;
|
|
33
|
+
|
|
34
|
+
if (trust_proxy) {
|
|
35
|
+
const forwarded_proto = request.headers.get('x-forwarded-proto');
|
|
36
|
+
if (forwarded_proto) {
|
|
37
|
+
protocol = forwarded_proto.split(',')[0].trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const forwarded_host = request.headers.get('x-forwarded-host');
|
|
41
|
+
if (forwarded_host) {
|
|
42
|
+
host = forwarded_host.split(',')[0].trim();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `${protocol}://${host}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Global fetch patching
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Quick check whether a string looks like it already has a URL scheme.
|
|
55
|
+
* @param {string} url
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
function has_scheme(url) {
|
|
59
|
+
return /^[a-z][a-z0-9+\-.]*:/i.test(url);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @type {patch_global_fetch} */
|
|
63
|
+
export function patch_global_fetch(async_context) {
|
|
64
|
+
// Guard: if fetch is already patched by Ripple, don't wrap it again.
|
|
65
|
+
// This prevents layered wrapping when createHandler() or getDevAsyncContext()
|
|
66
|
+
// is called more than once in the same process (tests, hot reload, etc.).
|
|
67
|
+
if (/** @type {RipplePatchedFetch} */ (globalThis.fetch).__ripple_patched) {
|
|
68
|
+
return () => {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @type {typeof globalThis.fetch} */
|
|
72
|
+
const original_fetch = globalThis.fetch;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string | Request | URL} input
|
|
76
|
+
* @param {RequestInit} [init]
|
|
77
|
+
* @returns {ReturnType<typeof globalThis.fetch>}
|
|
78
|
+
*/
|
|
79
|
+
const patched_fetch = function (input, init) {
|
|
80
|
+
const context = async_context.getStore();
|
|
81
|
+
|
|
82
|
+
if (context?.origin) {
|
|
83
|
+
if (typeof input === 'string' && !has_scheme(input)) {
|
|
84
|
+
input = new URL(input, context.origin).href;
|
|
85
|
+
} else if (input instanceof Request) {
|
|
86
|
+
const url = input.url;
|
|
87
|
+
if (!has_scheme(url)) {
|
|
88
|
+
input = new Request(new URL(url, context.origin).href, input);
|
|
89
|
+
}
|
|
90
|
+
} else if (input instanceof URL) {
|
|
91
|
+
if (!input.protocol || input.protocol === '' || input.origin === 'null') {
|
|
92
|
+
const relative = input.pathname + (input.search || '') + (input.hash || '');
|
|
93
|
+
input = new URL(relative, context.origin);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return original_fetch(input, init);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Copy static properties (e.g. fetch.preconnect) so the patched
|
|
102
|
+
// function satisfies the full `typeof fetch` contract.
|
|
103
|
+
Object.assign(patched_fetch, original_fetch);
|
|
104
|
+
|
|
105
|
+
// Mark as patched so subsequent calls are idempotent
|
|
106
|
+
/** @type {RipplePatchedFetch} */ (patched_fetch).__ripple_patched = true;
|
|
107
|
+
|
|
108
|
+
globalThis.fetch = /** @type {typeof globalThis.fetch} */ (patched_fetch);
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
globalThis.fetch = original_fetch;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// RPC lookup
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
/** @type {build_rpc_lookup} */
|
|
120
|
+
export function build_rpc_lookup(rpc_modules, hash_fn) {
|
|
121
|
+
/** @type {Map<string, RpcEntry>} */
|
|
122
|
+
const lookup = new Map();
|
|
123
|
+
|
|
124
|
+
for (const [entry_path, server_obj] of Object.entries(rpc_modules)) {
|
|
125
|
+
for (const func_name of Object.keys(server_obj)) {
|
|
126
|
+
const func_path = entry_path + '#' + func_name;
|
|
127
|
+
const hash = hash_fn(func_path);
|
|
128
|
+
lookup.set(hash, { serverObj: server_obj, funcName: func_name });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return lookup;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================================================
|
|
136
|
+
// RPC request handler
|
|
137
|
+
// ============================================================================
|
|
138
|
+
|
|
139
|
+
/** @type {is_rpc_request} */
|
|
140
|
+
export function is_rpc_request(pathname) {
|
|
141
|
+
return pathname.startsWith(RPC_PATH_PREFIX);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @type {handle_rpc_request} */
|
|
145
|
+
export async function handle_rpc_request(request, options) {
|
|
146
|
+
const { resolveFunction, executeServerFunction, asyncContext, trustProxy } = options;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const url = new URL(request.url);
|
|
150
|
+
const hash = url.pathname.slice(RPC_PATH_PREFIX.length);
|
|
151
|
+
|
|
152
|
+
// Validate hash format — compiler always generates 8 lowercase hex chars
|
|
153
|
+
if (!hash || !/^[a-f0-9]{8}$/.test(hash)) {
|
|
154
|
+
return new Response('Invalid RPC request', { status: 400 });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const body = await request.text();
|
|
158
|
+
|
|
159
|
+
const fn = await resolveFunction(hash);
|
|
160
|
+
if (!fn) {
|
|
161
|
+
return new Response(`RPC function not found: ${hash}`, { status: 404 });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const origin = derive_origin(request, trustProxy);
|
|
165
|
+
|
|
166
|
+
return await asyncContext.run({ origin }, async () => {
|
|
167
|
+
const result = await executeServerFunction(fn, body);
|
|
168
|
+
|
|
169
|
+
return new Response(result, {
|
|
170
|
+
status: 200,
|
|
171
|
+
headers: { 'Content-Type': 'application/json' },
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error('[ripple] RPC error:', error);
|
|
176
|
+
return new Response(
|
|
177
|
+
JSON.stringify({ error: error instanceof Error ? error.message : 'RPC failed' }),
|
|
178
|
+
{
|
|
179
|
+
status: 500,
|
|
180
|
+
headers: { 'Content-Type': 'application/json' },
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
package/tests/index.test.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
2
|
import {
|
|
6
3
|
DEFAULT_HOSTNAME,
|
|
7
4
|
DEFAULT_PORT,
|
|
@@ -10,7 +7,6 @@ import {
|
|
|
10
7
|
internal_server_error_response,
|
|
11
8
|
is_hashed_asset,
|
|
12
9
|
run_next_middleware,
|
|
13
|
-
serveStatic,
|
|
14
10
|
} from '../src/index.js';
|
|
15
11
|
|
|
16
12
|
describe('@ripple-ts/adapter', () => {
|
|
@@ -95,98 +91,4 @@ describe('@ripple-ts/adapter', () => {
|
|
|
95
91
|
);
|
|
96
92
|
expect(get_static_cache_control('/assets/app.js', 60, false)).toBe('public, max-age=60');
|
|
97
93
|
});
|
|
98
|
-
|
|
99
|
-
it('serveStatic serves files for GET and HEAD requests', async () => {
|
|
100
|
-
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-static-'));
|
|
101
|
-
try {
|
|
102
|
-
writeFileSync(join(temp_dir, 'app.js'), 'console.log("ok");');
|
|
103
|
-
|
|
104
|
-
const static_handler = serveStatic(temp_dir, { prefix: '/assets' });
|
|
105
|
-
|
|
106
|
-
const get_response = static_handler(new Request('http://localhost/assets/app.js'));
|
|
107
|
-
expect(get_response).not.toBeNull();
|
|
108
|
-
if (get_response === null) {
|
|
109
|
-
throw new Error('Expected static GET response');
|
|
110
|
-
}
|
|
111
|
-
expect(get_response.status).toBe(200);
|
|
112
|
-
expect(get_response.headers.get('content-type')).toBe('text/javascript; charset=utf-8');
|
|
113
|
-
expect(get_response.headers.get('cache-control')).toBe('public, max-age=86400');
|
|
114
|
-
expect(await get_response.text()).toContain('console.log');
|
|
115
|
-
|
|
116
|
-
const head_response = static_handler(
|
|
117
|
-
new Request('http://localhost/assets/app.js', { method: 'HEAD' }),
|
|
118
|
-
);
|
|
119
|
-
expect(head_response).not.toBeNull();
|
|
120
|
-
if (head_response === null) {
|
|
121
|
-
throw new Error('Expected static HEAD response');
|
|
122
|
-
}
|
|
123
|
-
expect(head_response.status).toBe(200);
|
|
124
|
-
expect(await head_response.text()).toBe('');
|
|
125
|
-
} finally {
|
|
126
|
-
rmSync(temp_dir, { recursive: true, force: true });
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('serveStatic falls through for non-matching requests', () => {
|
|
131
|
-
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-static-fallthrough-'));
|
|
132
|
-
try {
|
|
133
|
-
writeFileSync(join(temp_dir, 'index.html'), '<h1>ok</h1>');
|
|
134
|
-
const static_handler = serveStatic(temp_dir, { prefix: '/assets' });
|
|
135
|
-
|
|
136
|
-
expect(static_handler(new Request('http://localhost/index.html'))).toBeNull();
|
|
137
|
-
expect(static_handler(new Request('http://localhost/assets/missing.js'))).toBeNull();
|
|
138
|
-
expect(
|
|
139
|
-
static_handler(new Request('http://localhost/assets/index.html', { method: 'POST' })),
|
|
140
|
-
).toBeNull();
|
|
141
|
-
} finally {
|
|
142
|
-
rmSync(temp_dir, { recursive: true, force: true });
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('serveStatic applies configured cache-control options', async () => {
|
|
147
|
-
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-static-cache-'));
|
|
148
|
-
try {
|
|
149
|
-
writeFileSync(join(temp_dir, 'app.js'), 'console.log("cache");');
|
|
150
|
-
|
|
151
|
-
const max_age_handler = serveStatic(temp_dir, { prefix: '/assets', maxAge: 120 });
|
|
152
|
-
const max_age_response = max_age_handler(new Request('http://localhost/assets/app.js'));
|
|
153
|
-
expect(max_age_response).not.toBeNull();
|
|
154
|
-
if (max_age_response === null) {
|
|
155
|
-
throw new Error('Expected static response');
|
|
156
|
-
}
|
|
157
|
-
expect(max_age_response.headers.get('cache-control')).toBe('public, max-age=120');
|
|
158
|
-
|
|
159
|
-
const immutable_handler = serveStatic(temp_dir, {
|
|
160
|
-
prefix: '/assets',
|
|
161
|
-
maxAge: 120,
|
|
162
|
-
immutable: true,
|
|
163
|
-
});
|
|
164
|
-
const immutable_response = immutable_handler(new Request('http://localhost/assets/app.js'));
|
|
165
|
-
expect(immutable_response).not.toBeNull();
|
|
166
|
-
if (immutable_response === null) {
|
|
167
|
-
throw new Error('Expected static response');
|
|
168
|
-
}
|
|
169
|
-
expect(immutable_response.headers.get('cache-control')).toBe(
|
|
170
|
-
'public, max-age=31536000, immutable',
|
|
171
|
-
);
|
|
172
|
-
} finally {
|
|
173
|
-
rmSync(temp_dir, { recursive: true, force: true });
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('serveStatic blocks traversal and directory targets', () => {
|
|
178
|
-
const temp_dir = mkdtempSync(join(tmpdir(), 'adapter-static-security-'));
|
|
179
|
-
try {
|
|
180
|
-
const public_dir = join(temp_dir, 'public');
|
|
181
|
-
mkdirSync(public_dir);
|
|
182
|
-
mkdirSync(join(public_dir, 'nested'));
|
|
183
|
-
writeFileSync(join(temp_dir, 'secret.txt'), 'secret');
|
|
184
|
-
|
|
185
|
-
const static_handler = serveStatic(public_dir, { prefix: '/assets' });
|
|
186
|
-
expect(static_handler(new Request('http://localhost/assets/%2e%2e/secret.txt'))).toBeNull();
|
|
187
|
-
expect(static_handler(new Request('http://localhost/assets/nested'))).toBeNull();
|
|
188
|
-
} finally {
|
|
189
|
-
rmSync(temp_dir, { recursive: true, force: true });
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
94
|
});
|
package/types/index.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ export type FetchHandler<Platform = any, ResultValue = Response> = (
|
|
|
3
3
|
platform?: Platform,
|
|
4
4
|
) => ResultValue | Promise<ResultValue>;
|
|
5
5
|
|
|
6
|
+
// Re-export runtime primitive types from rpc module for adapter authors
|
|
7
|
+
export type { RuntimePrimitives, AsyncContext } from './rpc.js';
|
|
8
|
+
|
|
6
9
|
export type AdapterCoreOptions = {
|
|
7
10
|
port?: number;
|
|
8
11
|
hostname?: string;
|
|
@@ -70,8 +73,3 @@ export function get_static_cache_control(
|
|
|
70
73
|
): string;
|
|
71
74
|
|
|
72
75
|
export function get_mime_type(pathname: string): string;
|
|
73
|
-
|
|
74
|
-
export function serveStatic(
|
|
75
|
-
dir: string,
|
|
76
|
-
options?: ServeStaticOptions,
|
|
77
|
-
): (request: Request) => Response | null;
|
package/types/rpc.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async context abstraction — wraps platform-specific implementations
|
|
3
|
+
* (e.g., Node.js AsyncLocalStorage, Bun AsyncLocalStorage).
|
|
4
|
+
*/
|
|
5
|
+
export type AsyncContext<T = any> = {
|
|
6
|
+
/** Run a function with the given store value visible to getStore() */
|
|
7
|
+
run: <R>(store: T, fn: () => R | Promise<R>) => R | Promise<R>;
|
|
8
|
+
/** Get the current store value (undefined if outside a run() call) */
|
|
9
|
+
getStore: () => T | undefined;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Platform-specific runtime primitives provided by each adapter.
|
|
14
|
+
*
|
|
15
|
+
* These allow the shared server runtime to operate without depending
|
|
16
|
+
* on Node.js-specific APIs like `node:crypto` or `node:async_hooks`.
|
|
17
|
+
*/
|
|
18
|
+
export type RuntimePrimitives = {
|
|
19
|
+
/**
|
|
20
|
+
* Hash a string for RPC function identification.
|
|
21
|
+
*
|
|
22
|
+
* Must produce the same output as the compiler's ServerBlock transform:
|
|
23
|
+
* SHA-256 hex digest truncated to 8 characters.
|
|
24
|
+
*
|
|
25
|
+
* @param str - The string to hash (typically "filePath#funcName")
|
|
26
|
+
* @returns The hash string (8 hex chars)
|
|
27
|
+
*/
|
|
28
|
+
hash: (str: string) => string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a request-scoped async context.
|
|
32
|
+
*
|
|
33
|
+
* Used to propagate the request origin through async call stacks
|
|
34
|
+
* so that the patched `fetch` can resolve relative URLs.
|
|
35
|
+
*/
|
|
36
|
+
createAsyncContext: <T = any>() => AsyncContext<T>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Entry in the RPC lookup table.
|
|
41
|
+
*/
|
|
42
|
+
export type RpcEntry = {
|
|
43
|
+
serverObj: Record<string, Function>;
|
|
44
|
+
funcName: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Options for handle_rpc_request.
|
|
49
|
+
*/
|
|
50
|
+
export type HandleRpcOptions = {
|
|
51
|
+
/** Resolve a hash to the server function to call */
|
|
52
|
+
resolveFunction: (hash: string) => Function | null | Promise<Function | null>;
|
|
53
|
+
/** Execute a resolved server function with the request body */
|
|
54
|
+
executeServerFunction: (fn: Function, body: string) => Promise<string>;
|
|
55
|
+
/** Request-scoped async context for fetch patching */
|
|
56
|
+
asyncContext: AsyncContext;
|
|
57
|
+
/** Whether to trust X-Forwarded-* headers */
|
|
58
|
+
trustProxy: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Derive the request origin (protocol + host) from a Web Request.
|
|
63
|
+
* Honors proxy headers only when trustProxy is true.
|
|
64
|
+
*/
|
|
65
|
+
export function derive_origin(request: Request, trust_proxy: boolean): string;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Patch globalThis.fetch to resolve relative URLs using the async context.
|
|
69
|
+
* Returns a cleanup function that restores the original fetch.
|
|
70
|
+
*/
|
|
71
|
+
export function patch_global_fetch(async_context: AsyncContext): () => void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a hash → RpcEntry lookup from rpcModules.
|
|
75
|
+
*/
|
|
76
|
+
export function build_rpc_lookup(
|
|
77
|
+
rpc_modules: Record<string, Record<string, Function>>,
|
|
78
|
+
hash_fn: (str: string) => string,
|
|
79
|
+
): Map<string, RpcEntry>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check whether a URL pathname is an RPC request.
|
|
83
|
+
*/
|
|
84
|
+
export function is_rpc_request(pathname: string): boolean;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handle an RPC request. Platform-agnostic (Web Request/Response).
|
|
88
|
+
*/
|
|
89
|
+
export function handle_rpc_request(request: Request, options: HandleRpcOptions): Promise<Response>;
|
|
90
|
+
|
|
91
|
+
export type RipplePatchedFetch = typeof globalThis.fetch & {
|
|
92
|
+
__ripple_patched?: boolean;
|
|
93
|
+
};
|