@base44/functions-compiler 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/README.md +137 -0
- package/lib/dist/activation-shim.mjs +237 -0
- package/lib/dist/actor.mjs +1129 -0
- package/lib/dist/deno-shim.mjs +2347 -0
- package/lib/src/actor-compat.d.ts +24 -0
- package/lib/src/actor-compat.js +302 -0
- package/lib/src/actor-compat.js.map +1 -0
- package/lib/src/bundler.d.ts +51 -0
- package/lib/src/bundler.js +321 -0
- package/lib/src/bundler.js.map +1 -0
- package/lib/src/contracts.d.ts +108 -0
- package/lib/src/contracts.js +39 -0
- package/lib/src/contracts.js.map +1 -0
- package/lib/src/deno-bundle.d.ts +30 -0
- package/lib/src/deno-bundle.js +146 -0
- package/lib/src/deno-bundle.js.map +1 -0
- package/lib/src/errors.d.ts +16 -0
- package/lib/src/errors.js +11 -0
- package/lib/src/errors.js.map +1 -0
- package/lib/src/esbuild/deno-resolver.d.ts +26 -0
- package/lib/src/esbuild/deno-resolver.js +436 -0
- package/lib/src/esbuild/deno-resolver.js.map +1 -0
- package/lib/src/esbuild/node-builtin-require.d.ts +2 -0
- package/lib/src/esbuild/node-builtin-require.js +33 -0
- package/lib/src/esbuild/node-builtin-require.js.map +1 -0
- package/lib/src/esbuild/private-data-sources-virtual.d.ts +3 -0
- package/lib/src/esbuild/private-data-sources-virtual.js +127 -0
- package/lib/src/esbuild/private-data-sources-virtual.js.map +1 -0
- package/lib/src/esbuild/runtime-context-virtual.d.ts +3 -0
- package/lib/src/esbuild/runtime-context-virtual.js +42 -0
- package/lib/src/esbuild/runtime-context-virtual.js.map +1 -0
- package/lib/src/esbuild/runtime-virtual.d.ts +2 -0
- package/lib/src/esbuild/runtime-virtual.js +52 -0
- package/lib/src/esbuild/runtime-virtual.js.map +1 -0
- package/lib/src/esbuild/user-files.d.ts +15 -0
- package/lib/src/esbuild/user-files.js +73 -0
- package/lib/src/esbuild/user-files.js.map +1 -0
- package/lib/src/fetch-guard.d.ts +26 -0
- package/lib/src/fetch-guard.js +67 -0
- package/lib/src/fetch-guard.js.map +1 -0
- package/lib/src/index.d.ts +12 -0
- package/lib/src/index.js +11 -0
- package/lib/src/index.js.map +1 -0
- package/lib/src/log.d.ts +7 -0
- package/lib/src/log.js +39 -0
- package/lib/src/log.js.map +1 -0
- package/lib/src/private-data-sources/build-http.ts +51 -0
- package/lib/src/private-data-sources/elasticsearch.ts +8 -0
- package/lib/src/private-data-sources/http-url.ts +23 -0
- package/lib/src/private-data-sources/http.ts +6 -0
- package/lib/src/private-data-sources/hyperdrive.ts +48 -0
- package/lib/src/private-data-sources/ioredis-adapter.ts +229 -0
- package/lib/src/private-data-sources/manifest.ts +150 -0
- package/lib/src/private-data-sources/mariadb.ts +8 -0
- package/lib/src/private-data-sources/mongodb.ts +70 -0
- package/lib/src/private-data-sources/mysql.ts +8 -0
- package/lib/src/private-data-sources/node-net-adapter.ts +244 -0
- package/lib/src/private-data-sources/postgres.ts +8 -0
- package/lib/src/private-data-sources/redis-auth.ts +83 -0
- package/lib/src/private-data-sources/redis.ts +47 -0
- package/lib/src/private-data-sources/runtime-environment.ts +4 -0
- package/lib/src/private-data-sources/runtime-manifest-store.ts +32 -0
- package/lib/src/private-data-sources/sqlserver.ts +22 -0
- package/lib/src/private-data-sources/tcp.ts +86 -0
- package/lib/src/private-data-sources/tedious-adapter.ts +67 -0
- package/lib/src/private-data-sources/types.ts +29 -0
- package/lib/src/runtime/index.ts +30 -0
- package/lib/src/runtime-context.ts +29 -0
- package/lib/src/static-egress-marker.d.ts +1 -0
- package/lib/src/static-egress-marker.js +7 -0
- package/lib/src/static-egress-marker.js.map +1 -0
- package/lib/src/telemetry.d.ts +32 -0
- package/lib/src/telemetry.js +84 -0
- package/lib/src/telemetry.js.map +1 -0
- package/lib/src/tracing.d.ts +9 -0
- package/lib/src/tracing.js +21 -0
- package/lib/src/tracing.js.map +1 -0
- package/lib/src/worker-entry.d.ts +36 -0
- package/lib/src/worker-entry.js +320 -0
- package/lib/src/worker-entry.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import nodeNet from "node:net";
|
|
3
|
+
import { Duplex } from "node:stream";
|
|
4
|
+
|
|
5
|
+
import { isCloudflareTcpSocket } from "./tcp";
|
|
6
|
+
|
|
7
|
+
interface NodeNetSocket extends Duplex {
|
|
8
|
+
connecting: boolean;
|
|
9
|
+
remoteAddress?: string;
|
|
10
|
+
remotePort?: number;
|
|
11
|
+
setNoDelay(noDelay?: boolean): NodeNetSocket;
|
|
12
|
+
setKeepAlive(enable?: boolean, initialDelay?: number): NodeNetSocket;
|
|
13
|
+
setTimeout(timeout: number, callback?: () => void): NodeNetSocket;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface PrivateNodeNetRoute {
|
|
17
|
+
label: string;
|
|
18
|
+
connect: () => unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const privateNodeNetRoutes = new Map<string, PrivateNodeNetRoute>();
|
|
22
|
+
|
|
23
|
+
function exactNodeBuffer(value: Uint8Array): Buffer {
|
|
24
|
+
const bytes = new Uint8Array(value.byteLength);
|
|
25
|
+
bytes.set(value);
|
|
26
|
+
return Buffer.from(bytes.buffer);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function nodeNetRouteKey(hostname: string, port: number): string {
|
|
30
|
+
return `${hostname.toLowerCase()}:${port}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function registerPrivateNodeNetRoute(hostname: string, port: number, route: PrivateNodeNetRoute) {
|
|
34
|
+
privateNodeNetRoutes.set(nodeNetRouteKey(hostname, port), route);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseNodeNetCreateConnectionAddress(args: unknown[]) {
|
|
38
|
+
const [first, second] = args;
|
|
39
|
+
if (first && typeof first === "object") {
|
|
40
|
+
const options = first as { host?: unknown; hostname?: unknown; port?: unknown; path?: unknown };
|
|
41
|
+
if (typeof options.path === "string") return null;
|
|
42
|
+
const port = typeof options.port === "number" ? options.port : Number(options.port);
|
|
43
|
+
if (!Number.isFinite(port)) return null;
|
|
44
|
+
const hostname =
|
|
45
|
+
typeof options.host === "string"
|
|
46
|
+
? options.host
|
|
47
|
+
: typeof options.hostname === "string"
|
|
48
|
+
? options.hostname
|
|
49
|
+
: "localhost";
|
|
50
|
+
return { hostname, port };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof first === "number") {
|
|
54
|
+
const hostname = typeof second === "string" ? second : "localhost";
|
|
55
|
+
return { hostname, port: first };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseNodeNetConnectCallback(args: unknown[]): (() => void) | undefined {
|
|
62
|
+
const callback = args.find((arg) => typeof arg === "function");
|
|
63
|
+
return callback as (() => void) | undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function installPrivateNodeNetAdapter() {
|
|
67
|
+
const netModule = nodeNet as typeof nodeNet & {
|
|
68
|
+
__base44PrivateDataSourceAdapterInstalled?: boolean;
|
|
69
|
+
};
|
|
70
|
+
if (netModule.__base44PrivateDataSourceAdapterInstalled) return;
|
|
71
|
+
|
|
72
|
+
const originalCreateConnection = netModule.createConnection.bind(netModule);
|
|
73
|
+
const createConnection = (...args: unknown[]) => {
|
|
74
|
+
const address = parseNodeNetCreateConnectionAddress(args);
|
|
75
|
+
const route = address
|
|
76
|
+
? privateNodeNetRoutes.get(nodeNetRouteKey(address.hostname, address.port))
|
|
77
|
+
: undefined;
|
|
78
|
+
if (!route) {
|
|
79
|
+
return originalCreateConnection(...(args as Parameters<typeof nodeNet.createConnection>));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const socket = buildNodeNetSocket(route.connect(), route.label);
|
|
83
|
+
const callback = parseNodeNetConnectCallback(args);
|
|
84
|
+
if (callback) socket.once("connect", callback);
|
|
85
|
+
return socket;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
netModule.createConnection = createConnection as typeof nodeNet.createConnection;
|
|
89
|
+
netModule.connect = createConnection as typeof nodeNet.connect;
|
|
90
|
+
netModule.__base44PrivateDataSourceAdapterInstalled = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildNodeNetSocket(
|
|
94
|
+
socket: unknown,
|
|
95
|
+
label: string,
|
|
96
|
+
options: { deferConnectEvent?: boolean } = {},
|
|
97
|
+
): NodeNetSocket {
|
|
98
|
+
if (!isCloudflareTcpSocket(socket)) {
|
|
99
|
+
throw new Error(`${label} did not return a readable/writable TCP socket`);
|
|
100
|
+
}
|
|
101
|
+
const cloudflareSocket = socket;
|
|
102
|
+
|
|
103
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
104
|
+
let writer: WritableStreamDefaultWriter<Uint8Array> | null = null;
|
|
105
|
+
let closed = false;
|
|
106
|
+
let connectEmitted = false;
|
|
107
|
+
let readableEnded = false;
|
|
108
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
109
|
+
let writeChain = Promise.resolve();
|
|
110
|
+
|
|
111
|
+
function clearTimer() {
|
|
112
|
+
if (!timeout) return;
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
timeout = null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function releaseLocks() {
|
|
118
|
+
try {
|
|
119
|
+
reader?.releaseLock();
|
|
120
|
+
} catch {
|
|
121
|
+
// Ignore release errors when the stream is already closed.
|
|
122
|
+
}
|
|
123
|
+
reader = null;
|
|
124
|
+
try {
|
|
125
|
+
writer?.releaseLock();
|
|
126
|
+
} catch {
|
|
127
|
+
// Ignore release errors when the stream is already closed.
|
|
128
|
+
}
|
|
129
|
+
writer = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function endReadable() {
|
|
133
|
+
if (readableEnded) return;
|
|
134
|
+
readableEnded = true;
|
|
135
|
+
stream.push(null);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const stream = new Duplex({
|
|
139
|
+
read() {
|
|
140
|
+
// Data is pushed from the Cloudflare readable stream below.
|
|
141
|
+
},
|
|
142
|
+
write(chunk, _encoding, callback) {
|
|
143
|
+
const bytes = chunk instanceof Uint8Array ? chunk : Buffer.from(chunk);
|
|
144
|
+
writeChain = writeChain
|
|
145
|
+
.then(async () => {
|
|
146
|
+
if (closed) throw new Error(`${label} connection is closed`);
|
|
147
|
+
const activeWriter = writer ?? cloudflareSocket.writable.getWriter();
|
|
148
|
+
writer = activeWriter;
|
|
149
|
+
await activeWriter.write(bytes);
|
|
150
|
+
})
|
|
151
|
+
.then(
|
|
152
|
+
() => callback(),
|
|
153
|
+
(error) => callback(error instanceof Error ? error : new Error(String(error))),
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
final(callback) {
|
|
157
|
+
writeChain
|
|
158
|
+
.then(() => {
|
|
159
|
+
try {
|
|
160
|
+
cloudflareSocket.close?.();
|
|
161
|
+
} finally {
|
|
162
|
+
callback();
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
.catch((error) => callback(error instanceof Error ? error : new Error(String(error))));
|
|
166
|
+
},
|
|
167
|
+
destroy(error, callback) {
|
|
168
|
+
closed = true;
|
|
169
|
+
clearTimer();
|
|
170
|
+
try {
|
|
171
|
+
cloudflareSocket.close?.();
|
|
172
|
+
} catch {
|
|
173
|
+
// The stream is already being destroyed; surface the original error below.
|
|
174
|
+
}
|
|
175
|
+
releaseLocks();
|
|
176
|
+
callback(error);
|
|
177
|
+
},
|
|
178
|
+
}) as NodeNetSocket;
|
|
179
|
+
|
|
180
|
+
stream.connecting = true;
|
|
181
|
+
stream.setNoDelay = () => stream;
|
|
182
|
+
stream.setKeepAlive = () => stream;
|
|
183
|
+
stream.setTimeout = (duration, callback) => {
|
|
184
|
+
clearTimer();
|
|
185
|
+
if (duration > 0) {
|
|
186
|
+
timeout = setTimeout(() => {
|
|
187
|
+
callback?.();
|
|
188
|
+
stream.emit("timeout");
|
|
189
|
+
}, duration);
|
|
190
|
+
}
|
|
191
|
+
return stream;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
async function readLoop() {
|
|
195
|
+
const activeReader = reader ?? cloudflareSocket.readable.getReader();
|
|
196
|
+
reader = activeReader;
|
|
197
|
+
try {
|
|
198
|
+
while (!closed) {
|
|
199
|
+
const { value, done } = await activeReader.read();
|
|
200
|
+
if (done) break;
|
|
201
|
+
if (value) stream.push(exactNodeBuffer(value));
|
|
202
|
+
}
|
|
203
|
+
endReadable();
|
|
204
|
+
releaseLocks();
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (!closed) stream.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function markConnected(info?: unknown) {
|
|
211
|
+
if (closed || connectEmitted) return;
|
|
212
|
+
connectEmitted = true;
|
|
213
|
+
const socketInfo = info as { remoteAddress?: unknown; remotePort?: unknown } | null;
|
|
214
|
+
if (typeof socketInfo?.remoteAddress === "string") stream.remoteAddress = socketInfo.remoteAddress;
|
|
215
|
+
if (typeof socketInfo?.remotePort === "number") stream.remotePort = socketInfo.remotePort;
|
|
216
|
+
stream.connecting = false;
|
|
217
|
+
stream.emit("connect");
|
|
218
|
+
readLoop();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function scheduleConnected(info?: unknown) {
|
|
222
|
+
if (options.deferConnectEvent) {
|
|
223
|
+
setTimeout(() => markConnected(info), 0);
|
|
224
|
+
} else {
|
|
225
|
+
markConnected(info);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (cloudflareSocket.opened) {
|
|
230
|
+
cloudflareSocket.opened.then(
|
|
231
|
+
(info) => scheduleConnected(info),
|
|
232
|
+
(error) => stream.destroy(error instanceof Error ? error : new Error(String(error))),
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
queueMicrotask(() => scheduleConnected());
|
|
236
|
+
}
|
|
237
|
+
cloudflareSocket.closed?.finally(() => {
|
|
238
|
+
closed = true;
|
|
239
|
+
clearTimer();
|
|
240
|
+
endReadable();
|
|
241
|
+
releaseLocks();
|
|
242
|
+
}).catch((error) => stream.destroy(error instanceof Error ? error : new Error(String(error))));
|
|
243
|
+
return stream;
|
|
244
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { buildHyperdrivePrivateDataSource } from "./hyperdrive";
|
|
2
|
+
import { privateDataSourceReference } from "./manifest";
|
|
3
|
+
|
|
4
|
+
export function postgres(name: unknown) {
|
|
5
|
+
return buildHyperdrivePrivateDataSource(
|
|
6
|
+
privateDataSourceReference(name, "postgres"),
|
|
7
|
+
);
|
|
8
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { CloudflareTcpSocket } from "./types";
|
|
2
|
+
|
|
3
|
+
const MAX_AUTH_RESPONSE_BYTES = 4096;
|
|
4
|
+
|
|
5
|
+
function isCloudflareTcpSocket(socket: unknown): socket is CloudflareTcpSocket {
|
|
6
|
+
const candidate = socket as Partial<CloudflareTcpSocket> | null;
|
|
7
|
+
return (
|
|
8
|
+
candidate !== null &&
|
|
9
|
+
typeof candidate === "object" &&
|
|
10
|
+
!!candidate.readable &&
|
|
11
|
+
typeof candidate.readable.getReader === "function" &&
|
|
12
|
+
!!candidate.writable &&
|
|
13
|
+
typeof candidate.writable.getWriter === "function"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function encodeRespArray(values: string[]): Uint8Array {
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
const chunks = [encoder.encode(`*${values.length}\r\n`)];
|
|
20
|
+
for (const value of values) {
|
|
21
|
+
const bytes = encoder.encode(value);
|
|
22
|
+
chunks.push(encoder.encode(`$${bytes.byteLength}\r\n`), bytes, encoder.encode("\r\n"));
|
|
23
|
+
}
|
|
24
|
+
const result = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.byteLength, 0));
|
|
25
|
+
let offset = 0;
|
|
26
|
+
for (const chunk of chunks) {
|
|
27
|
+
result.set(chunk, offset);
|
|
28
|
+
offset += chunk.byteLength;
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readResponseLine(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<string> {
|
|
34
|
+
const bytes: number[] = [];
|
|
35
|
+
while (bytes.length <= MAX_AUTH_RESPONSE_BYTES) {
|
|
36
|
+
const { value, done } = await reader.read();
|
|
37
|
+
if (done) break;
|
|
38
|
+
for (const byte of value ?? []) {
|
|
39
|
+
bytes.push(byte);
|
|
40
|
+
const length = bytes.length;
|
|
41
|
+
if (length >= 2 && bytes[length - 2] === 13 && bytes[length - 1] === 10) {
|
|
42
|
+
return new TextDecoder().decode(new Uint8Array(bytes.slice(0, -2)));
|
|
43
|
+
}
|
|
44
|
+
if (length > MAX_AUTH_RESPONSE_BYTES) break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error("Redis authentication failed: invalid or missing server response");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function authenticateRedisSocket(
|
|
51
|
+
socket: unknown,
|
|
52
|
+
username: string | undefined,
|
|
53
|
+
password: string,
|
|
54
|
+
): Promise<CloudflareTcpSocket> {
|
|
55
|
+
if (!isCloudflareTcpSocket(socket)) {
|
|
56
|
+
throw new Error("Redis private data source did not return a readable/writable TCP socket");
|
|
57
|
+
}
|
|
58
|
+
const reader = socket.readable.getReader();
|
|
59
|
+
const writer = socket.writable.getWriter();
|
|
60
|
+
try {
|
|
61
|
+
await socket.opened;
|
|
62
|
+
const auth = username ? ["AUTH", username, password] : ["AUTH", password];
|
|
63
|
+
await writer.write(encodeRespArray(auth));
|
|
64
|
+
const response = await readResponseLine(reader);
|
|
65
|
+
if (response === "+OK") return socket;
|
|
66
|
+
const message = response.startsWith("-") ? response.slice(1) : `unexpected response ${JSON.stringify(response)}`;
|
|
67
|
+
throw new Error(`Redis authentication failed: ${message}`);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
socket.close?.();
|
|
70
|
+
throw error;
|
|
71
|
+
} finally {
|
|
72
|
+
reader.releaseLock();
|
|
73
|
+
writer.releaseLock();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function authenticateRedisSocketIfNeeded(
|
|
78
|
+
socket: unknown,
|
|
79
|
+
username: string | undefined,
|
|
80
|
+
password: string | undefined,
|
|
81
|
+
): unknown | Promise<CloudflareTcpSocket> {
|
|
82
|
+
return password ? authenticateRedisSocket(socket, username, password) : socket;
|
|
83
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { buildIoredisConnectorFactory } from "./ioredis-adapter";
|
|
2
|
+
import { privateDataSourceReference } from "./manifest";
|
|
3
|
+
import { authenticateRedisSocketIfNeeded } from "./redis-auth";
|
|
4
|
+
import { buildTcpPrivateDataSource } from "./tcp";
|
|
5
|
+
|
|
6
|
+
export function redis(name: unknown) {
|
|
7
|
+
const source = buildTcpPrivateDataSource(
|
|
8
|
+
privateDataSourceReference(name, "redis"),
|
|
9
|
+
) as ReturnType<typeof buildTcpPrivateDataSource> & {
|
|
10
|
+
connect: (options?: unknown) => unknown;
|
|
11
|
+
};
|
|
12
|
+
const { connect: _connect, ...descriptors } =
|
|
13
|
+
Object.getOwnPropertyDescriptors(source);
|
|
14
|
+
const extension = {
|
|
15
|
+
connect(options?: unknown) {
|
|
16
|
+
return authenticateRedisSocketIfNeeded(
|
|
17
|
+
source.connect(options),
|
|
18
|
+
source.username,
|
|
19
|
+
source.password,
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
ioredisConnector() {
|
|
23
|
+
return buildIoredisConnectorFactory(() =>
|
|
24
|
+
authenticateRedisSocketIfNeeded(
|
|
25
|
+
source.connect(),
|
|
26
|
+
source.username,
|
|
27
|
+
source.password,
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
ioredisOptions() {
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
Connector: buildIoredisConnectorFactory(() => source.connect()),
|
|
34
|
+
...(source.password && source.username
|
|
35
|
+
? { username: source.username }
|
|
36
|
+
: {}),
|
|
37
|
+
...(source.password ? { password: source.password } : {}),
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return Object.freeze(
|
|
42
|
+
Object.defineProperties(
|
|
43
|
+
extension,
|
|
44
|
+
descriptors,
|
|
45
|
+
) as typeof source & typeof extension,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Private, single-instance channel for the handshake-delivered PDS manifest.
|
|
2
|
+
//
|
|
3
|
+
// The manifest carries plaintext VPC/DB credentials, so it must NOT be
|
|
4
|
+
// reachable by user code. It lives here in module-private state: the activation
|
|
5
|
+
// shim writes it (via `setRuntimeManifest`) and `manifest.ts` reads it (via
|
|
6
|
+
// `getRuntimeManifest`) — both resolve to THIS one module instance in the final
|
|
7
|
+
// bundle, so the value never touches `globalThis`, `process.env`, or any
|
|
8
|
+
// user-importable surface. User function code cannot import this module, via
|
|
9
|
+
// two independent gates: (1) the bundler's private-data-sources virtual plugin
|
|
10
|
+
// resolves the `base44:private-data-sources/runtime-manifest-store` specifier
|
|
11
|
+
// ONLY when the importer is EXACTLY the injected activation shim's bundle-root
|
|
12
|
+
// key (not a suffix/segment match — else a nested `x/__base44_activation.mjs`
|
|
13
|
+
// could pose as it); (2) `assertNoReservedFilenames` rejects that basename at
|
|
14
|
+
// any depth in user files. The relative `./runtime-manifest-store` path only
|
|
15
|
+
// resolves inside the plugin's namespace (the adapters + manifest.ts), never
|
|
16
|
+
// from a user file.
|
|
17
|
+
//
|
|
18
|
+
// Single instance: both importers resolve to the SAME (namespace, path) pair,
|
|
19
|
+
// so esbuild emits one module. The runtime-secrets e2e proves this end to end —
|
|
20
|
+
// if the two ever diverged into separate instances, the shim would write one
|
|
21
|
+
// and manifest.ts would read the other (empty), and the delivered manifest
|
|
22
|
+
// would resolve as "not bound".
|
|
23
|
+
|
|
24
|
+
let runtimeManifestRaw: string | undefined;
|
|
25
|
+
|
|
26
|
+
export function setRuntimeManifest(raw: string | undefined): void {
|
|
27
|
+
runtimeManifestRaw = raw;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getRuntimeManifest(): string | undefined {
|
|
31
|
+
return runtimeManifestRaw;
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { buildTediousConnectorFactory } from "./tedious-adapter";
|
|
2
|
+
import { privateDataSourceReference } from "./manifest";
|
|
3
|
+
import { buildTcpPrivateDataSource } from "./tcp";
|
|
4
|
+
|
|
5
|
+
export function sqlserver(name: unknown) {
|
|
6
|
+
const source = buildTcpPrivateDataSource(
|
|
7
|
+
privateDataSourceReference(name, "sqlserver"),
|
|
8
|
+
) as ReturnType<typeof buildTcpPrivateDataSource> & {
|
|
9
|
+
connect: (options?: unknown) => unknown;
|
|
10
|
+
};
|
|
11
|
+
const extension = {
|
|
12
|
+
tediousConnector() {
|
|
13
|
+
return buildTediousConnectorFactory(() => source.connect());
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
return Object.freeze(
|
|
17
|
+
Object.defineProperties(
|
|
18
|
+
extension,
|
|
19
|
+
Object.getOwnPropertyDescriptors(source),
|
|
20
|
+
) as typeof source & typeof extension,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
currentPrivateDataSource,
|
|
3
|
+
privateDataSourceReference,
|
|
4
|
+
privateDataSourceBinding,
|
|
5
|
+
} from "./manifest";
|
|
6
|
+
import type {
|
|
7
|
+
CloudflareTcpSocket,
|
|
8
|
+
PrivateDataSourceManifestEntry,
|
|
9
|
+
TcpPrivateDataSourceBinding,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
function hasConnect(binding: unknown): binding is TcpPrivateDataSourceBinding {
|
|
13
|
+
return (
|
|
14
|
+
binding !== null &&
|
|
15
|
+
typeof binding === "object" &&
|
|
16
|
+
typeof (binding as { connect?: unknown }).connect === "function"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildTcpPrivateDataSource(entry: PrivateDataSourceManifestEntry) {
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
name: entry.name,
|
|
23
|
+
type: entry.type,
|
|
24
|
+
get bindingName() {
|
|
25
|
+
return currentPrivateDataSource(entry).bindingName;
|
|
26
|
+
},
|
|
27
|
+
get host() {
|
|
28
|
+
return currentPrivateDataSource(entry).host;
|
|
29
|
+
},
|
|
30
|
+
get port() {
|
|
31
|
+
return currentPrivateDataSource(entry).port;
|
|
32
|
+
},
|
|
33
|
+
get database() {
|
|
34
|
+
return currentPrivateDataSource(entry).database;
|
|
35
|
+
},
|
|
36
|
+
get username() {
|
|
37
|
+
return currentPrivateDataSource(entry).username;
|
|
38
|
+
},
|
|
39
|
+
get user() {
|
|
40
|
+
return currentPrivateDataSource(entry).username;
|
|
41
|
+
},
|
|
42
|
+
get password() {
|
|
43
|
+
return currentPrivateDataSource(entry).password;
|
|
44
|
+
},
|
|
45
|
+
connect(options?: unknown) {
|
|
46
|
+
const current = currentPrivateDataSource(entry);
|
|
47
|
+
const binding = privateDataSourceBinding(current);
|
|
48
|
+
if (!hasConnect(binding)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Private data source "${current.name}" does not expose connect()`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const address =
|
|
54
|
+
typeof current.host === "string" && typeof current.port === "number"
|
|
55
|
+
? { hostname: current.host, port: current.port }
|
|
56
|
+
: null;
|
|
57
|
+
if (!address) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Private data source "${current.name}" has no TCP address`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return binding.connect(address, options);
|
|
63
|
+
},
|
|
64
|
+
get binding() {
|
|
65
|
+
return privateDataSourceBinding(entry);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function isCloudflareTcpSocket(socket: unknown): socket is CloudflareTcpSocket {
|
|
71
|
+
const socketObject = socket as Partial<CloudflareTcpSocket> | null;
|
|
72
|
+
return (
|
|
73
|
+
socketObject !== null &&
|
|
74
|
+
typeof socketObject === "object" &&
|
|
75
|
+
!!socketObject.readable &&
|
|
76
|
+
typeof socketObject.readable.getReader === "function" &&
|
|
77
|
+
!!socketObject.writable &&
|
|
78
|
+
typeof socketObject.writable.getWriter === "function"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function tcp(name: unknown, expectedType: string) {
|
|
83
|
+
return buildTcpPrivateDataSource(
|
|
84
|
+
privateDataSourceReference(name, expectedType),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isCloudflareTcpSocket } from "./tcp";
|
|
2
|
+
import { buildNodeNetSocket } from "./node-net-adapter";
|
|
3
|
+
|
|
4
|
+
function abortError(label: string, signal: AbortSignal): Error {
|
|
5
|
+
return signal.reason instanceof Error
|
|
6
|
+
? signal.reason
|
|
7
|
+
: new Error(`${label} connection aborted`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function waitForCloudflareTcpSocketOpen(
|
|
11
|
+
socket: unknown,
|
|
12
|
+
label: string,
|
|
13
|
+
signal?: AbortSignal,
|
|
14
|
+
) {
|
|
15
|
+
if (!isCloudflareTcpSocket(socket)) {
|
|
16
|
+
throw new Error(`${label} did not return a readable/writable TCP socket`);
|
|
17
|
+
}
|
|
18
|
+
if (!socket.opened) return;
|
|
19
|
+
|
|
20
|
+
if (!signal) {
|
|
21
|
+
await socket.opened;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (signal.aborted) {
|
|
26
|
+
try {
|
|
27
|
+
socket.close?.();
|
|
28
|
+
} catch {
|
|
29
|
+
// Best-effort cleanup; surface the abort reason below.
|
|
30
|
+
}
|
|
31
|
+
throw abortError(label, signal);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let onAbort: (() => void) | null = null;
|
|
35
|
+
try {
|
|
36
|
+
await Promise.race([
|
|
37
|
+
socket.opened,
|
|
38
|
+
new Promise((_, reject) => {
|
|
39
|
+
onAbort = () => reject(abortError(label, signal));
|
|
40
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
41
|
+
}),
|
|
42
|
+
]);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
try {
|
|
45
|
+
socket.close?.();
|
|
46
|
+
} catch {
|
|
47
|
+
// Preserve the original connection/abort error.
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
} finally {
|
|
51
|
+
if (onAbort) signal.removeEventListener("abort", onAbort);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildTediousNetSocket(socket: unknown) {
|
|
56
|
+
return buildNodeNetSocket(socket, "SQL Server private data source", {
|
|
57
|
+
deferConnectEvent: true,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildTediousConnectorFactory(connect: () => unknown) {
|
|
62
|
+
return async (_connectOptions?: unknown, _lookup?: unknown, signal?: AbortSignal) => {
|
|
63
|
+
const socket = connect();
|
|
64
|
+
await waitForCloudflareTcpSocketOpen(socket, "SQL Server private data source", signal);
|
|
65
|
+
return buildTediousNetSocket(socket);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface PrivateDataSourceManifestEntry {
|
|
2
|
+
name?: string;
|
|
3
|
+
type?: string;
|
|
4
|
+
bindingName?: string;
|
|
5
|
+
bindingKind?: string;
|
|
6
|
+
networkScope?: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
host?: string;
|
|
9
|
+
port?: number;
|
|
10
|
+
database?: string;
|
|
11
|
+
username?: string;
|
|
12
|
+
password?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FetchPrivateDataSourceBinding {
|
|
16
|
+
fetch: (resource: RequestInfo | URL, init?: RequestInit) => Response | Promise<Response>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TcpPrivateDataSourceBinding {
|
|
20
|
+
connect: (address: unknown, options?: unknown) => unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CloudflareTcpSocket {
|
|
24
|
+
readable: ReadableStream<Uint8Array>;
|
|
25
|
+
writable: WritableStream<Uint8Array>;
|
|
26
|
+
opened?: Promise<unknown>;
|
|
27
|
+
close?: () => void;
|
|
28
|
+
closed?: Promise<unknown>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Platform module served for `import { ... } from "base44:runtime"`.
|
|
2
|
+
//
|
|
3
|
+
// The generated Worker entry owns the per-request AsyncLocalStorage store
|
|
4
|
+
// (module-scoped there), so this module reaches request state through the
|
|
5
|
+
// `globalThis.Base44` bridge the entry prelude installs — same pattern as the
|
|
6
|
+
// private-data-sources modules reading their manifest.
|
|
7
|
+
|
|
8
|
+
interface Base44Bridge {
|
|
9
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
10
|
+
secrets: { get(name: string): string | undefined };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function bridge(): Base44Bridge {
|
|
14
|
+
return (globalThis as { Base44?: Base44Bridge }).Base44 as Base44Bridge;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Extend this invocation until `promise` settles — background work after the
|
|
18
|
+
* Response returns. Rides ctx.waitUntil; best-effort, not durable. Returns the
|
|
19
|
+
* same promise so it composes. */
|
|
20
|
+
export function waitUntil<T>(promise: Promise<T>): Promise<T> {
|
|
21
|
+
bridge().waitUntil(promise);
|
|
22
|
+
return promise;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** App secrets, read from the Worker env binding of the current request. */
|
|
26
|
+
export const secrets: { get(name: string): string | undefined } = {
|
|
27
|
+
get(name: string): string | undefined {
|
|
28
|
+
return bridge().secrets.get(name);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
export interface WorkerRuntimeContext {
|
|
4
|
+
env: "prod" | "preview";
|
|
5
|
+
fn?: string;
|
|
6
|
+
secrets: Record<string, unknown>;
|
|
7
|
+
workerEnv: Record<string, unknown>;
|
|
8
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const runtimeContext = new AsyncLocalStorage<WorkerRuntimeContext>();
|
|
13
|
+
|
|
14
|
+
export function currentWorkerRuntimeContext():
|
|
15
|
+
| WorkerRuntimeContext
|
|
16
|
+
| undefined {
|
|
17
|
+
return runtimeContext.getStore();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function workerEnvironment(): Record<string, unknown> {
|
|
21
|
+
return currentWorkerRuntimeContext()?.workerEnv ?? {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function runWithWorkerEnvironment<T>(
|
|
25
|
+
context: WorkerRuntimeContext,
|
|
26
|
+
callback: () => T,
|
|
27
|
+
): T {
|
|
28
|
+
return runtimeContext.run(context, callback);
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const STATIC_EGRESS_ARTIFACT_MARKER = "base44.static-egress.request-env.v2";
|