Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3937098
try again pgcrypto on CI
tdrz Dec 11, 2025
8f53e17
add openpgp
tdrz Dec 11, 2025
fc162f2
style
tdrz Dec 11, 2025
a99c273
update submodule
tdrz Dec 11, 2025
35e2bc0
udpate submodule
tdrz Dec 11, 2025
a7d7ce0
print error messages on pglite socket test
tdrz Dec 11, 2025
7cb160f
CI: do not fail fast
tdrz Dec 11, 2025
43c5085
test using node v22.20.0
tdrz Dec 11, 2025
de9dcca
test using node v22.20.0
tdrz Dec 11, 2025
c4866b4
update submodule; disable pgcrypto test
tdrz Dec 11, 2025
11d05fe
readd pgcrypto test
tdrz Dec 11, 2025
4fdd284
update submodule
tdrz Dec 11, 2025
98340f2
try to identify what function call is missing
tdrz Dec 11, 2025
d814208
update submodule
tdrz Dec 11, 2025
f648ebc
update submodule
tdrz Dec 11, 2025
c3d349a
update submodule
tdrz Dec 11, 2025
088c7ea
trying to find missing symbol
tdrz Dec 11, 2025
45d7005
update submodule
tdrz Dec 11, 2025
986e954
update submodule
tdrz Dec 11, 2025
21804b9
update submodule
tdrz Dec 11, 2025
d46771d
remove removeFunction calls on pglite end
tdrz Dec 11, 2025
b55618e
undo changes to CI workflow
tdrz Dec 16, 2025
380b815
update submodule
tdrz Dec 16, 2025
3a00287
undo pglite.ts changes
tdrz Dec 16, 2025
3458462
more pgcrypto tests
tdrz Dec 16, 2025
e0fe161
update size
tdrz Dec 16, 2025
c7e6d88
remove wasmTable
tdrz Dec 16, 2025
d865dcb
update submodule
tdrz Dec 16, 2025
5de09e3
style
tdrz Dec 16, 2025
8775a24
update submodule
tdrz Dec 16, 2025
2f062db
merge main
tdrz Jan 6, 2026
8959a02
udpate submodule
tdrz Jan 6, 2026
0c683bc
update submodule
Jan 6, 2026
80115d9
changeset
Jan 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/nervous-seahorses-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@electric-sql/pglite-socket': patch
'@electric-sql/pglite': patch
---

added pgcrypto extension
14 changes: 14 additions & 0 deletions docs/extensions/extensions.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,20 @@ const baseExtensions: Extension[] = [
importName: 'pg_ivm',
size: 24865,
},
{
name: 'pgcrypto',
description: `
The pgcrypto module provides cryptographic functions for PostgreSQL.
`,
shortDescription:
'The pgcrypto module provides cryptographic functions for PostgreSQL.',
docs: 'https://www.postgresql.org/docs/current/pgcrypto.html',
tags: ['postgres extension', 'postgres/contrib'],
importPath: '@electric-sql/pglite/contrib/pgcrypto',
importName: 'pgcrypto',
core: true,
size: 1148162,
},
{
name: 'pg_hashids',
description: `
Expand Down
3 changes: 2 additions & 1 deletion docs/repl/allExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ export { pg_buffercache } from '@electric-sql/pglite/contrib/pg_buffercache'
export { pg_freespacemap } from '@electric-sql/pglite/contrib/pg_freespacemap'
export { pg_surgery } from '@electric-sql/pglite/contrib/pg_surgery'
export { pg_trgm } from '@electric-sql/pglite/contrib/pg_trgm'
export { pg_uuidv7 } from '@electric-sql/pglite/pg_uuidv7'
export { pg_visibility } from '@electric-sql/pglite/contrib/pg_visibility'
export { pg_walinspect } from '@electric-sql/pglite/contrib/pg_walinspect'
export { pgcrypto } from '@electric-sql/pglite/contrib/pgcrypto'
export { pgtap } from '@electric-sql/pglite/pgtap'
export { pg_uuidv7 } from '@electric-sql/pglite/pg_uuidv7'
export { seg } from '@electric-sql/pglite/contrib/seg'
export { tablefunc } from '@electric-sql/pglite/contrib/tablefunc'
export { tcn } from '@electric-sql/pglite/contrib/tcn'
Expand Down
20 changes: 20 additions & 0 deletions packages/pglite-socket/tests/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ describe('Server Script Tests', () => {
output += data.toString()
})

serverProcess.stderr?.on('data', (data) => {
console.error(data.toString())
})

await new Promise<void>((resolve) => {
serverProcess.on('exit', (code) => {
expect(code).toBe(0)
Expand Down Expand Up @@ -78,6 +82,10 @@ describe('Server Script Tests', () => {
output += data.toString()
})

serverProcess.stderr?.on('data', (data) => {
console.error(data.toString())
})

// Wait for server to start
await waitForPort(testPort)

Expand Down Expand Up @@ -126,6 +134,10 @@ describe('Server Script Tests', () => {
output += data.toString()
})

serverProcess.stderr?.on('data', (data) => {
console.error(data.toString())
})

// Wait for server to be ready
const isReady = await waitForPort(testPort)
expect(isReady).toBe(true)
Expand Down Expand Up @@ -159,6 +171,10 @@ describe('Server Script Tests', () => {
output += data.toString()
})

serverProcess.stderr?.on('data', (data) => {
console.error(data.toString())
})

const isReady = await waitForPort(testPort)
expect(isReady).toBe(true)
expect(output).toContain('Initializing PGLite with database: memory://')
Expand Down Expand Up @@ -198,6 +214,10 @@ describe('Server Script Tests', () => {
output += data.toString()
})

serverProcess.stderr?.on('data', (data) => {
console.error(data.toString())
})

const isReady = await waitForPort(testPort)
expect(isReady).toBe(true)
serverProcess.kill()
Expand Down
1 change: 1 addition & 0 deletions packages/pglite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@
"bun": "^1.1.30",
"concurrently": "^8.2.2",
"http-server": "^14.1.1",
"openpgp": "^6.3.0",
"playwright": "^1.48.0",
"tinytar": "^0.1.0",
"vitest": "^2.1.2"
Expand Down
16 changes: 16 additions & 0 deletions packages/pglite/src/contrib/pgcrypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type {
Extension,
ExtensionSetupResult,
PGliteInterface,
} from '../interface'

const setup = async (_pg: PGliteInterface, _emscriptenOpts: any) => {
return {
bundlePath: new URL('../../release/pgcrypto.tar.gz', import.meta.url),
} satisfies ExtensionSetupResult
}

export const pgcrypto = {
name: 'pgcrypto',
setup,
} satisfies Extension
252 changes: 252 additions & 0 deletions packages/pglite/tests/contrib/pgcrypto.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import { describe, it, expect } from 'vitest'
import { PGlite } from '../../dist/index.js'
import { pgcrypto } from '../../dist/contrib/pgcrypto.js'
import * as openpgp from 'openpgp'

describe('pg_pgcryptotrgm', () => {
it('digest', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(
"SELECT encode(digest(convert_to('test', 'UTF8'), 'sha1'), 'hex') as value;",
)
expect(res.rows[0].value, 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3')
})

it('hmac', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(
"SELECT encode(hmac(convert_to('test', 'UTF8'), convert_to('key', 'UTF8'), 'sha1'), 'hex') as value;",
)
expect(res.rows[0].value).toEqual(
'671f54ce0c540f78ffe1e26dcf9c2a047aea4fda',
)
})

it('crypt', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query("SELECT crypt('test', gen_salt('bf')) as value;")
expect(res.rows[0].value.length).toEqual(60)
})

it('gen_salt', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query("SELECT gen_salt('bf') as value;")
expect(res.rows[0].value.length).toEqual(29)
})

it('armor', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query("SELECT armor(digest('test', 'sha1')) as value;")
expect(res.rows[0].value).toContain('-----BEGIN PGP MESSAGE-----')
expect(res.rows[0].value).toContain('-----END PGP MESSAGE-----')
})

it('pgp_sym_encrypt and pgp_sym_decrypt', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(
"SELECT pgp_sym_encrypt('test', 'key') as value;",
)
const encrypted = res.rows[0].value

const res2 = await pg.query("SELECT pgp_sym_decrypt($1, 'key') as value;", [
encrypted,
])
expect(res2.rows[0].value).toEqual('test')
})

it('pgp_pub_encrypt and pgp_pub_decrypt', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const { privateKey, publicKey } = await openpgp.generateKey({
type: 'rsa',
rsaBits: 2048,
userIDs: [{ name: 'PGlite', email: 'hello@pglite.dev' }],
passphrase: '',
})

const toEncrypt = 'PGlite@$#%!^$&*WQFgjqPkVERewfreg094340f1012-='

const e2 = await pg.exec(
`
WITH encrypted AS (
SELECT pgp_pub_encrypt('${toEncrypt}', dearmor('${publicKey}')) AS encrypted
)
SELECT
pgp_pub_decrypt(encrypted, dearmor('${privateKey}')) as decrypted_output
FROM encrypted;
`,
)
expect(e2[0].rows[0].decrypted_output, toEncrypt)
})
Comment on lines +99 to +128
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect test!


it('pgp_key_id', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const { publicKey } = await openpgp.generateKey({
type: 'rsa',
rsaBits: 2048,
userIDs: [{ name: 'PGlite', email: 'hello@pglite.dev' }],
passphrase: '',
})

const res = await pg.query(
`SELECT pgp_key_id(dearmor('${publicKey}')) as value;`,
)
// pgp_key_id returns a 16-character hex string
expect(res.rows[0].value).toHaveLength(16)
expect(res.rows[0].value).toMatch(/^[0-9A-F]+$/)
})

it('pgp_armor_headers', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

// Create armored data with headers
const res = await pg.query(
`SELECT armor(digest('test', 'sha1'), ARRAY['key1'], ARRAY['value1']) as armored;`,
)
const armored = res.rows[0].armored

const res2 = await pg.query(`SELECT * FROM pgp_armor_headers($1);`, [
armored,
])
expect(res2.rows).toContainEqual({ key: 'key1', value: 'value1' })
})

it('encrypt and decrypt', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(
`SELECT encrypt('test data'::bytea, 'secret key'::bytea, 'aes') as encrypted;`,
)
const encrypted = res.rows[0].encrypted

const res2 = await pg.query(
`SELECT convert_from(decrypt($1, 'secret key'::bytea, 'aes'), 'UTF8') as decrypted;`,
[encrypted],
)
expect(res2.rows[0].decrypted).toEqual('test data')
})

it('encrypt_iv and decrypt_iv', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

// AES block size is 16 bytes, so IV must be 16 bytes
const iv = '1234567890123456'

const res = await pg.query(
`SELECT encrypt_iv('test data'::bytea, 'secret key'::bytea, '${iv}'::bytea, 'aes') as encrypted;`,
)
const encrypted = res.rows[0].encrypted

const res2 = await pg.query(
`SELECT convert_from(decrypt_iv($1, 'secret key'::bytea, '${iv}'::bytea, 'aes'), 'UTF8') as decrypted;`,
[encrypted],
)
expect(res2.rows[0].decrypted).toEqual('test data')
})

it('gen_random_bytes', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(
`SELECT length(gen_random_bytes(32)) as len, encode(gen_random_bytes(16), 'hex') as bytes;`,
)
expect(res.rows[0].len).toEqual(32)
// 16 bytes = 32 hex characters
expect(res.rows[0].bytes).toHaveLength(32)
})

it('gen_random_uuid', async () => {
const pg = new PGlite({
extensions: {
pgcrypto,
},
})

await pg.exec('CREATE EXTENSION IF NOT EXISTS pgcrypto;')

const res = await pg.query(`SELECT gen_random_uuid() as uuid;`)
// UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
expect(res.rows[0].uuid).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
)
})
})
Loading