Skip to content

Commit b88e261

Browse files
porsagerpaulovieirashayan-shojaeiwackfxwackfx
authored
Support for Cloudflare Workers & Pages (#599)
* Initial support for cloudflare * Types here are not needed * Include cloudflare in npm * Allow crypto to be async to support WebCrypto polyfills * Polyfill crypto with WebCrypto for cloudflare * Use crypto polyfill for cloudflare * Not ready for tests on CF yet * build * build cf * build * README.md - improve the "Multiple statements in one query" section - add links for the official documentation - escape the backtick character - change the subtitle to "await sql``.simple()" instead of "await sql`select 1; select 2;`.simple()" (to be coherent with the other subtitles) - add a small example below * Ensure number options are coerced from string - fixes #622 * Add sql.reserve method * build * create beginPrepared function (#628) * create beginPrepared function * change implementation to new method * add prepare method type to TransactionSql * add documentations and test * fix test * enable prepared transactions in the bootstrap script * enable prepared transactions in the github actions setup file * fix github actions * fix github actions yml file * Please the linter * build * Fix for using compatibility_flags = [ "nodejs_compat" ] instead * build * please eslint * draft: Cloudflare works ! 🎉 (#618) * Reworked from source cloudflare branch feat: reran transpile fix linter feat: final touches + test files squashed 2 commits fix: Polyfills bulk (to please linter) fix: Removed MD5 + put back SHA in the digest() squashed 5 commits fix: cloudflare workers deployment feat: fixed auth fix: encrypt not found in worker :( fix: postgres SASL fix: linting * fix: merge cleanup --------- Co-authored-by: wackfx <[email protected]> * Switch to performance.now * Please the linter * Don't collect polyfills (keep line numbers similar to src) * Simplify manual test script * build --------- Co-authored-by: Paulo Vieira <[email protected]> Co-authored-by: Shayan Shojaei <[email protected]> Co-authored-by: Wack <[email protected]> Co-authored-by: wackfx <[email protected]>
1 parent 94f7228 commit b88e261

18 files changed

+2996
-31
lines changed

.eslintrc.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@
214214
],
215215
"max-len": [
216216
2,
217-
120
217+
150
218218
],
219219
"max-nested-callbacks": [
220220
2,

cf/polyfills.js

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { EventEmitter } from 'node:events'
2+
import { Buffer } from 'node:buffer'
3+
4+
const Crypto = globalThis.crypto
5+
6+
let ids = 1
7+
const tasks = new Set()
8+
9+
const v4Seg = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])'
10+
const v4Str = `(${v4Seg}[.]){3}${v4Seg}`
11+
const IPv4Reg = new RegExp(`^${v4Str}$`)
12+
13+
const v6Seg = '(?:[0-9a-fA-F]{1,4})'
14+
const IPv6Reg = new RegExp(
15+
'^(' +
16+
`(?:${v6Seg}:){7}(?:${v6Seg}|:)|` +
17+
`(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` +
18+
`(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|` +
19+
`(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|` +
20+
`(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|` +
21+
`(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|` +
22+
`(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|` +
23+
`(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` +
24+
')(%[0-9a-zA-Z-.:]{1,})?$'
25+
)
26+
27+
const textEncoder = new TextEncoder()
28+
export const crypto = {
29+
randomBytes: l => Crypto.getRandomValues(Buffer.alloc(l)),
30+
pbkdf2Sync: async(password, salt, iterations, keylen) =>
31+
Crypto.subtle.deriveBits(
32+
{
33+
name: 'PBKDF2',
34+
hash: 'SHA-256',
35+
salt,
36+
iterations
37+
},
38+
await Crypto.subtle.importKey(
39+
'raw',
40+
textEncoder.encode(password),
41+
'PBKDF2',
42+
false,
43+
['deriveBits']
44+
),
45+
keylen * 8,
46+
['deriveBits']
47+
),
48+
createHash: type => ({
49+
update: x => ({
50+
digest: () => {
51+
if (type !== 'sha256')
52+
throw Error('createHash only supports sha256 on cloudflare.')
53+
if (!(x instanceof Uint8Array))
54+
x = textEncoder.encode(x)
55+
return Crypto.subtle.digest('SHA-256', x)
56+
}
57+
})
58+
}),
59+
createHmac: (type, key) => ({
60+
update: x => ({
61+
digest: async() =>
62+
Buffer.from(
63+
await Crypto.subtle.sign(
64+
'HMAC',
65+
await Crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']),
66+
textEncoder.encode(x)
67+
)
68+
)
69+
})
70+
})
71+
}
72+
73+
export const process = {
74+
env: {}
75+
}
76+
77+
export const os = {
78+
userInfo() {
79+
return { username: 'postgres' }
80+
}
81+
}
82+
83+
export const fs = {
84+
readFile() {
85+
throw new Error('Reading files not supported on CloudFlare')
86+
}
87+
}
88+
89+
export const net = {
90+
isIP: (x) => RegExp.prototype.test.call(IPv4Reg, x) ? 4 : RegExp.prototype.test.call(IPv6Reg, x) ? 6 : 0,
91+
Socket
92+
}
93+
94+
export { setImmediate, clearImmediate }
95+
96+
export const tls = {
97+
connect({ socket: tcp, servername }) {
98+
tcp.writer.releaseLock()
99+
tcp.reader.releaseLock()
100+
tcp.readyState = 'upgrading'
101+
tcp.raw = tcp.raw.startTls({ servername })
102+
tcp.raw.closed.then(
103+
() => tcp.emit('close'),
104+
(e) => tcp.emit('error', e)
105+
)
106+
tcp.writer = tcp.raw.writable.getWriter()
107+
tcp.reader = tcp.raw.readable.getReader()
108+
109+
tcp.writer.ready.then(() => {
110+
tcp.read()
111+
tcp.readyState = 'upgrade'
112+
})
113+
return tcp
114+
}
115+
}
116+
117+
function Socket() {
118+
const tcp = Object.assign(new EventEmitter(), {
119+
readyState: 'open',
120+
raw: null,
121+
writer: null,
122+
reader: null,
123+
connect,
124+
write,
125+
end,
126+
destroy,
127+
read
128+
})
129+
130+
return tcp
131+
132+
async function connect(port, host) {
133+
try {
134+
tcp.readyState = 'opening'
135+
const { connect } = await import('cloudflare:sockets')
136+
tcp.raw = connect(host + ':' + port, tcp.ssl ? { secureTransport: 'starttls' } : {})
137+
tcp.raw.closed.then(
138+
() => {
139+
tcp.readyState !== 'upgrade'
140+
? close()
141+
: ((tcp.readyState = 'open'), tcp.emit('secureConnect'))
142+
},
143+
(e) => tcp.emit('error', e)
144+
)
145+
tcp.writer = tcp.raw.writable.getWriter()
146+
tcp.reader = tcp.raw.readable.getReader()
147+
148+
tcp.ssl ? readFirst() : read()
149+
tcp.writer.ready.then(() => {
150+
tcp.readyState = 'open'
151+
tcp.emit('connect')
152+
})
153+
} catch (err) {
154+
error(err)
155+
}
156+
}
157+
158+
function close() {
159+
if (tcp.readyState === 'closed')
160+
return
161+
162+
tcp.readyState = 'closed'
163+
tcp.emit('close')
164+
}
165+
166+
function write(data, cb) {
167+
tcp.writer.write(data).then(cb, error)
168+
return true
169+
}
170+
171+
function end(data) {
172+
return data
173+
? tcp.write(data, () => tcp.raw.close())
174+
: tcp.raw.close()
175+
}
176+
177+
function destroy() {
178+
tcp.destroyed = true
179+
tcp.end()
180+
}
181+
182+
async function read() {
183+
try {
184+
let done
185+
, value
186+
while (({ done, value } = await tcp.reader.read(), !done))
187+
tcp.emit('data', Buffer.from(value))
188+
} catch (err) {
189+
error(err)
190+
}
191+
}
192+
193+
async function readFirst() {
194+
const { value } = await tcp.reader.read()
195+
tcp.emit('data', Buffer.from(value))
196+
}
197+
198+
function error(err) {
199+
tcp.emit('error', err)
200+
tcp.emit('close')
201+
}
202+
}
203+
204+
function setImmediate(fn) {
205+
const id = ids++
206+
tasks.add(id)
207+
queueMicrotask(() => {
208+
if (tasks.has(id)) {
209+
fn()
210+
tasks.delete(id)
211+
}
212+
})
213+
return id
214+
}
215+
216+
function clearImmediate(id) {
217+
tasks.delete(id)
218+
}

cf/src/bytes.js

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Buffer } from 'node:buffer'
2+
const size = 256
3+
let buffer = Buffer.allocUnsafe(size)
4+
5+
const messages = 'BCcDdEFfHPpQSX'.split('').reduce((acc, x) => {
6+
const v = x.charCodeAt(0)
7+
acc[x] = () => {
8+
buffer[0] = v
9+
b.i = 5
10+
return b
11+
}
12+
return acc
13+
}, {})
14+
15+
const b = Object.assign(reset, messages, {
16+
N: String.fromCharCode(0),
17+
i: 0,
18+
inc(x) {
19+
b.i += x
20+
return b
21+
},
22+
str(x) {
23+
const length = Buffer.byteLength(x)
24+
fit(length)
25+
b.i += buffer.write(x, b.i, length, 'utf8')
26+
return b
27+
},
28+
i16(x) {
29+
fit(2)
30+
buffer.writeUInt16BE(x, b.i)
31+
b.i += 2
32+
return b
33+
},
34+
i32(x, i) {
35+
if (i || i === 0) {
36+
buffer.writeUInt32BE(x, i)
37+
return b
38+
}
39+
fit(4)
40+
buffer.writeUInt32BE(x, b.i)
41+
b.i += 4
42+
return b
43+
},
44+
z(x) {
45+
fit(x)
46+
buffer.fill(0, b.i, b.i + x)
47+
b.i += x
48+
return b
49+
},
50+
raw(x) {
51+
buffer = Buffer.concat([buffer.subarray(0, b.i), x])
52+
b.i = buffer.length
53+
return b
54+
},
55+
end(at = 1) {
56+
buffer.writeUInt32BE(b.i - at, at)
57+
const out = buffer.subarray(0, b.i)
58+
b.i = 0
59+
buffer = Buffer.allocUnsafe(size)
60+
return out
61+
}
62+
})
63+
64+
export default b
65+
66+
function fit(x) {
67+
if (buffer.length - b.i < x) {
68+
const prev = buffer
69+
, length = prev.length
70+
71+
buffer = Buffer.allocUnsafe(length + (length >> 1) + x)
72+
prev.copy(buffer)
73+
}
74+
}
75+
76+
function reset() {
77+
b.i = 0
78+
return b
79+
}

0 commit comments

Comments
 (0)