@pgpm/encrypted-secrets-table 0.15.3 → 0.15.5

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/Makefile CHANGED
@@ -1,5 +1,5 @@
1
1
  EXTENSION = pgpm-encrypted-secrets-table
2
- DATA = sql/pgpm-encrypted-secrets-table--0.15.2.sql
2
+ DATA = sql/pgpm-encrypted-secrets-table--0.15.3.sql
3
3
 
4
4
  PG_CONFIG = pg_config
5
5
  PGXS := $(shell $(PG_CONFIG) --pgxs)
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  <img height="20" src="https://github.com/constructive-io/pgpm-modules/actions/workflows/ci.yml/badge.svg" />
10
10
  </a>
11
11
  <a href="https://github.com/constructive-io/pgpm-modules/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
12
- <a href="https://www.npmjs.com/package/@pgpm/encrypted-secrets-table"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/pgpm-modules?filename=packages%2Fsecurity%2Fencrypted-secrets-table%2Fpackage.json"/></a>
12
+ <a href="https://www.npmjs.com/package/@pgpm/encrypted-secrets-table"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/pgpm-modules?filename=packages%2Fencrypted-secrets-table%2Fpackage.json"/></a>
13
13
  </p>
14
14
 
15
15
  Table-based encrypted secrets storage and retrieval.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgpm/encrypted-secrets-table",
3
- "version": "0.15.3",
3
+ "version": "0.15.5",
4
4
  "description": "Table-based encrypted secrets storage and retrieval",
5
5
  "author": "Dan Lynch <pyramation@gmail.com>",
6
6
  "contributors": [
@@ -21,10 +21,10 @@
21
21
  "test:watch": "jest --watch"
22
22
  },
23
23
  "dependencies": {
24
- "@pgpm/verify": "0.15.3"
24
+ "@pgpm/verify": "0.15.5"
25
25
  },
26
26
  "devDependencies": {
27
- "pgpm": "^1.0.0"
27
+ "pgpm": "^1.3.0"
28
28
  },
29
29
  "repository": {
30
30
  "type": "git",
@@ -34,5 +34,5 @@
34
34
  "bugs": {
35
35
  "url": "https://github.com/constructive-io/pgpm-modules/issues"
36
36
  },
37
- "gitHead": "187ed37f6b731132fe930acf5b5996b1e63ecca0"
37
+ "gitHead": "f6bbdfb20760e308b02968038b6f54191a9fd527"
38
38
  }
@@ -1,6 +1,6 @@
1
1
  # pgpm-encrypted-secrets-table extension
2
2
  comment = 'pgpm-encrypted-secrets-table extension'
3
- default_version = '0.15.2'
3
+ default_version = '0.15.3'
4
4
  module_pathname = '$libdir/pgpm-encrypted-secrets-table'
5
5
  requires = 'pgcrypto,plpgsql,uuid-ossp,pgpm-verify'
6
6
  relocatable = false
@@ -1,38 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`encrypted secrets table should have secrets_table with correct structure 1`] = `
4
- {
5
- "columns": [
6
- {
7
- "column_default": "uuid_generate_v4()",
8
- "column_name": "id",
9
- "data_type": "uuid",
10
- "is_nullable": "NO",
11
- },
12
- {
13
- "column_default": null,
14
- "column_name": "secrets_owned_field",
15
- "data_type": "uuid",
16
- "is_nullable": "NO",
17
- },
18
- {
19
- "column_default": null,
20
- "column_name": "name",
21
- "data_type": "text",
22
- "is_nullable": "NO",
23
- },
24
- {
25
- "column_default": null,
26
- "column_name": "secrets_value_field",
27
- "data_type": "bytea",
28
- "is_nullable": "YES",
29
- },
30
- {
31
- "column_default": null,
32
- "column_name": "secrets_enc_field",
33
- "data_type": "text",
34
- "is_nullable": "YES",
35
- },
36
- ],
37
- }
38
- `;
@@ -1,272 +0,0 @@
1
- import { getConnections, PgTestClient, snapshot } from 'pgsql-test';
2
-
3
- let pg: PgTestClient;
4
- let teardown: () => Promise<void>;
5
-
6
- const user_id = 'dc474833-318a-41f5-9239-ee563ab657a6';
7
- const user_id_2 = '550e8400-e29b-41d4-a716-446655440000';
8
-
9
- describe('encrypted secrets table', () => {
10
- beforeAll(async () => {
11
- ({ pg, teardown } = await getConnections());
12
- });
13
-
14
- afterAll(async () => {
15
- await teardown();
16
- });
17
-
18
- beforeEach(async () => {
19
- await pg.beforeEach();
20
- });
21
-
22
- afterEach(async () => {
23
- await pg.afterEach();
24
- });
25
-
26
- it('should have secrets_schema created', async () => {
27
- const schemas = await pg.any(
28
- `SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'secrets_schema'`
29
- );
30
- expect(schemas).toHaveLength(1);
31
- expect(schemas[0].schema_name).toBe('secrets_schema');
32
- });
33
-
34
- it('should have secrets_table with correct structure', async () => {
35
- const columns = await pg.any(
36
- `SELECT column_name, data_type, is_nullable, column_default
37
- FROM information_schema.columns
38
- WHERE table_schema = 'secrets_schema' AND table_name = 'secrets_table'
39
- ORDER BY ordinal_position`
40
- );
41
-
42
- expect(snapshot({ columns })).toMatchSnapshot();
43
- });
44
-
45
- it('should have unique constraint on (secrets_owned_field, name)', async () => {
46
- const constraints = await pg.any(
47
- `SELECT constraint_name, constraint_type
48
- FROM information_schema.table_constraints
49
- WHERE table_schema = 'secrets_schema'
50
- AND table_name = 'secrets_table'
51
- AND constraint_type = 'UNIQUE'`
52
- );
53
-
54
- expect(constraints).toHaveLength(1);
55
- });
56
-
57
- it('should insert record with default values', async () => {
58
- const [result] = await pg.any(
59
- `INSERT INTO secrets_schema.secrets_table
60
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
61
- VALUES ($1::uuid, 'test-secret', 'test-value'::bytea, 'none')
62
- RETURNING *`,
63
- [user_id]
64
- );
65
-
66
- expect(result.secrets_owned_field).toBe(user_id);
67
- expect(result.name).toBe('test-secret');
68
- expect(result.secrets_enc_field).toBe('none');
69
- expect(result.id).toBeDefined();
70
- });
71
-
72
- it('should enforce unique constraint on (secrets_owned_field, name)', async () => {
73
- // Insert first record
74
- await pg.any(
75
- `INSERT INTO secrets_schema.secrets_table
76
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
77
- VALUES ($1::uuid, 'duplicate-name', 'value1'::bytea, 'none')`,
78
- [user_id]
79
- );
80
-
81
- // Try to insert duplicate - should fail
82
- await expect(
83
- pg.any(
84
- `INSERT INTO secrets_schema.secrets_table
85
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
86
- VALUES ($1::uuid, 'duplicate-name', 'value2'::bytea, 'none')`,
87
- [user_id]
88
- )
89
- ).rejects.toThrow();
90
- });
91
-
92
- it('should allow same name for different users', async () => {
93
- // Insert for first user
94
- await pg.any(
95
- `INSERT INTO secrets_schema.secrets_table
96
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
97
- VALUES ($1::uuid, 'same-name', 'value1'::bytea, 'none')`,
98
- [user_id]
99
- );
100
-
101
- // Insert for second user - should succeed
102
- const [result] = await pg.any(
103
- `INSERT INTO secrets_schema.secrets_table
104
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
105
- VALUES ($1::uuid, 'same-name', 'value2'::bytea, 'none')
106
- RETURNING *`,
107
- [user_id_2]
108
- );
109
-
110
- expect(result.secrets_owned_field).toBe(user_id_2);
111
- expect(result.name).toBe('same-name');
112
- });
113
-
114
- it('should trigger hash_secrets on insert with crypt', async () => {
115
- const plaintext = 'my-secret-password';
116
-
117
- const [result] = await pg.any(
118
- `INSERT INTO secrets_schema.secrets_table
119
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
120
- VALUES ($1::uuid, 'crypt-secret', $2::bytea, 'crypt')
121
- RETURNING *`,
122
- [user_id, plaintext]
123
- );
124
-
125
- // The trigger should have hashed the value
126
- expect(result.secrets_enc_field).toBe('crypt');
127
- expect(result.secrets_value_field).not.toEqual(Buffer.from(plaintext));
128
-
129
- // Verify it's a bcrypt hash (starts with $2)
130
- const hashedValue = result.secrets_value_field.toString();
131
- expect(hashedValue).toMatch(/^\$2[aby]?\$/);
132
- });
133
-
134
- it('should trigger hash_secrets on insert with pgp', async () => {
135
- const plaintext = 'my-secret-data';
136
-
137
- const [result] = await pg.any(
138
- `INSERT INTO secrets_schema.secrets_table
139
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
140
- VALUES ($1::uuid, 'pgp-secret', $2::bytea, 'pgp')
141
- RETURNING *`,
142
- [user_id, plaintext]
143
- );
144
-
145
- // The trigger should have encrypted the value
146
- expect(result.secrets_enc_field).toBe('pgp');
147
- expect(result.secrets_value_field).not.toEqual(Buffer.from(plaintext));
148
-
149
- // Should be longer than original due to encryption
150
- expect(result.secrets_value_field.length).toBeGreaterThan(plaintext.length);
151
- });
152
-
153
- it('should default to none encryption when not specified', async () => {
154
- const plaintext = 'unencrypted-data';
155
-
156
- const [result] = await pg.any(
157
- `INSERT INTO secrets_schema.secrets_table
158
- (secrets_owned_field, name, secrets_value_field)
159
- VALUES ($1::uuid, 'none-secret', $2::bytea)
160
- RETURNING *`,
161
- [user_id, plaintext]
162
- );
163
-
164
- // The trigger should set enc_field to 'none'
165
- expect(result.secrets_enc_field).toBe('none');
166
- expect(result.secrets_value_field).toEqual(Buffer.from(plaintext));
167
- });
168
-
169
- it('should trigger hash_secrets on update when value changes', async () => {
170
- // Insert initial record
171
- const [initial] = await pg.any(
172
- `INSERT INTO secrets_schema.secrets_table
173
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
174
- VALUES ($1::uuid, 'update-test', 'initial-value'::bytea, 'none')
175
- RETURNING *`,
176
- [user_id]
177
- );
178
-
179
- // Update to use crypt encryption
180
- const [updated] = await pg.any(
181
- `UPDATE secrets_schema.secrets_table
182
- SET secrets_value_field = 'new-password'::bytea, secrets_enc_field = 'crypt'
183
- WHERE id = $1
184
- RETURNING *`,
185
- [initial.id]
186
- );
187
-
188
- // Should be encrypted now
189
- expect(updated.secrets_enc_field).toBe('crypt');
190
- expect(updated.secrets_value_field).not.toEqual(Buffer.from('new-password'));
191
-
192
- // Verify it's a bcrypt hash
193
- const hashedValue = updated.secrets_value_field.toString();
194
- expect(hashedValue).toMatch(/^\$2[aby]?\$/);
195
- });
196
-
197
- it('should not trigger hash_secrets on update when value unchanged', async () => {
198
- // Insert initial record with crypt
199
- const [initial] = await pg.any(
200
- `INSERT INTO secrets_schema.secrets_table
201
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
202
- VALUES ($1::uuid, 'no-update-test', 'password'::bytea, 'crypt')
203
- RETURNING *`,
204
- [user_id]
205
- );
206
-
207
- const originalHash = initial.secrets_value_field;
208
-
209
- // Update name only (not the value field)
210
- const [updated] = await pg.any(
211
- `UPDATE secrets_schema.secrets_table
212
- SET name = 'renamed-secret'
213
- WHERE id = $1
214
- RETURNING *`,
215
- [initial.id]
216
- );
217
-
218
- // Hash should remain the same
219
- expect(updated.secrets_value_field).toEqual(originalHash);
220
- expect(updated.name).toBe('renamed-secret');
221
- });
222
-
223
- it('should verify crypt hash works correctly', async () => {
224
- const password = 'test-password-123';
225
-
226
- // Insert with crypt
227
- const [result] = await pg.any(
228
- `INSERT INTO secrets_schema.secrets_table
229
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
230
- VALUES ($1::uuid, 'crypt-verify', $2::bytea, 'crypt')
231
- RETURNING *`,
232
- [user_id, password]
233
- );
234
-
235
- // The stored hash should be different from the original password
236
- const storedHash = result.secrets_value_field.toString();
237
- expect(storedHash).not.toBe(password);
238
-
239
- // Verify it's a proper bcrypt hash format
240
- expect(storedHash).toMatch(/^\$2[aby]?\$/);
241
-
242
- // Verify the hash is the expected length (bcrypt hashes are typically 60 characters)
243
- expect(storedHash.length).toBe(60);
244
-
245
- // Verify that inserting the same password again produces a different hash (salt should be different)
246
- const [result2] = await pg.any(
247
- `INSERT INTO secrets_schema.secrets_table
248
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
249
- VALUES ($1::uuid, 'crypt-verify-2', $2::bytea, 'crypt')
250
- RETURNING *`,
251
- [user_id, password]
252
- );
253
-
254
- const storedHash2 = result2.secrets_value_field.toString();
255
- expect(storedHash2).not.toBe(storedHash); // Different salt = different hash
256
- expect(storedHash2).toMatch(/^\$2[aby]?\$/);
257
- });
258
-
259
- it('should handle null values correctly', async () => {
260
- const [result] = await pg.any(
261
- `INSERT INTO secrets_schema.secrets_table
262
- (secrets_owned_field, name, secrets_value_field, secrets_enc_field)
263
- VALUES ($1::uuid, 'null-test', NULL, NULL)
264
- RETURNING *`,
265
- [user_id]
266
- );
267
-
268
- // Trigger should set enc_field to 'none' even when value is null
269
- expect(result.secrets_enc_field).toBe('none');
270
- expect(result.secrets_value_field).toBeNull();
271
- });
272
- });
package/jest.config.js DELETED
@@ -1,15 +0,0 @@
1
- /** @type {import('ts-jest').JestConfigWithTsJest} */
2
- module.exports = {
3
- preset: 'ts-jest',
4
- testEnvironment: 'node',
5
-
6
- // Match both __tests__ and colocated test files
7
- testMatch: ['**/?(*.)+(test|spec).{ts,tsx,js,jsx}'],
8
-
9
- // Ignore build artifacts and type declarations
10
- testPathIgnorePatterns: ['/dist/', '\\.d\\.ts$'],
11
- modulePathIgnorePatterns: ['<rootDir>/dist/'],
12
- watchPathIgnorePatterns: ['/dist/'],
13
-
14
- moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
15
- };