@elevasis/core 0.12.0 → 0.13.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.
@@ -413,7 +413,6 @@ type Database = {
413
413
  acq_deals: {
414
414
  Row: {
415
415
  activity_log: Json;
416
- cached_stage: string | null;
417
416
  closed_lost_at: string | null;
418
417
  closed_lost_reason: string | null;
419
418
  contact_email: string;
@@ -428,6 +427,7 @@ type Database = {
428
427
  organization_id: string;
429
428
  payment_link_sent_at: string | null;
430
429
  payment_received_at: string | null;
430
+ pipeline_key: string;
431
431
  proposal_data: Json | null;
432
432
  proposal_generated_at: string | null;
433
433
  proposal_pdf_url: string | null;
@@ -435,10 +435,11 @@ type Database = {
435
435
  proposal_reviewed_by: string | null;
436
436
  proposal_sent_at: string | null;
437
437
  proposal_signed_at: string | null;
438
- proposal_status: string | null;
439
438
  signature_envelope_id: string | null;
440
439
  source_list_id: string | null;
441
440
  source_type: string | null;
441
+ stage_key: string | null;
442
+ state_key: string | null;
442
443
  stripe_payment_id: string | null;
443
444
  stripe_payment_link: string | null;
444
445
  stripe_payment_link_id: string | null;
@@ -447,7 +448,6 @@ type Database = {
447
448
  };
448
449
  Insert: {
449
450
  activity_log?: Json;
450
- cached_stage?: string | null;
451
451
  closed_lost_at?: string | null;
452
452
  closed_lost_reason?: string | null;
453
453
  contact_email: string;
@@ -462,6 +462,7 @@ type Database = {
462
462
  organization_id: string;
463
463
  payment_link_sent_at?: string | null;
464
464
  payment_received_at?: string | null;
465
+ pipeline_key?: string;
465
466
  proposal_data?: Json | null;
466
467
  proposal_generated_at?: string | null;
467
468
  proposal_pdf_url?: string | null;
@@ -469,10 +470,11 @@ type Database = {
469
470
  proposal_reviewed_by?: string | null;
470
471
  proposal_sent_at?: string | null;
471
472
  proposal_signed_at?: string | null;
472
- proposal_status?: string | null;
473
473
  signature_envelope_id?: string | null;
474
474
  source_list_id?: string | null;
475
475
  source_type?: string | null;
476
+ stage_key?: string | null;
477
+ state_key?: string | null;
476
478
  stripe_payment_id?: string | null;
477
479
  stripe_payment_link?: string | null;
478
480
  stripe_payment_link_id?: string | null;
@@ -481,7 +483,6 @@ type Database = {
481
483
  };
482
484
  Update: {
483
485
  activity_log?: Json;
484
- cached_stage?: string | null;
485
486
  closed_lost_at?: string | null;
486
487
  closed_lost_reason?: string | null;
487
488
  contact_email?: string;
@@ -496,6 +497,7 @@ type Database = {
496
497
  organization_id?: string;
497
498
  payment_link_sent_at?: string | null;
498
499
  payment_received_at?: string | null;
500
+ pipeline_key?: string;
499
501
  proposal_data?: Json | null;
500
502
  proposal_generated_at?: string | null;
501
503
  proposal_pdf_url?: string | null;
@@ -503,10 +505,11 @@ type Database = {
503
505
  proposal_reviewed_by?: string | null;
504
506
  proposal_sent_at?: string | null;
505
507
  proposal_signed_at?: string | null;
506
- proposal_status?: string | null;
507
508
  signature_envelope_id?: string | null;
508
509
  source_list_id?: string | null;
509
510
  source_type?: string | null;
511
+ stage_key?: string | null;
512
+ state_key?: string | null;
510
513
  stripe_payment_id?: string | null;
511
514
  stripe_payment_link?: string | null;
512
515
  stripe_payment_link_id?: string | null;
@@ -2755,12 +2758,6 @@ type Database = {
2755
2758
  };
2756
2759
  Returns: boolean;
2757
2760
  };
2758
- is_org_admin: {
2759
- Args: {
2760
- org_id: string;
2761
- };
2762
- Returns: boolean;
2763
- };
2764
2761
  is_org_member: {
2765
2762
  Args: {
2766
2763
  org_id: string;
@@ -2915,6 +2912,14 @@ declare class RLSTestContext {
2915
2912
  * Create an organization membership
2916
2913
  */
2917
2914
  createMembership(userId: string, organizationId: string, role: Role): Promise<Membership>;
2915
+ /**
2916
+ * Assign a system role to a membership via org_rol_assignments.
2917
+ * After the 2026-04-25 auth refactor, RLS policies read `effective_permissions[]`
2918
+ * (materialized by trigger from org_rol_assignments → org_rol_grants).
2919
+ * Without this assignment, role_slug is informational only and the membership
2920
+ * has zero permissions, causing all has_org_permission() checks to deny.
2921
+ */
2922
+ private assignSystemRole;
2918
2923
  /**
2919
2924
  * Create a pre-provisioned organization membership (without WorkOS membership ID)
2920
2925
  * Used for testing invitation flows where memberships are created before user accepts
@@ -1991,6 +1991,7 @@ var RLSTestContext = class {
1991
1991
  throw new Error(`Failed to create membership: ${error.message}`);
1992
1992
  }
1993
1993
  this.createdIds.memberships.push(data.id);
1994
+ await this.assignSystemRole(data.id, role);
1994
1995
  return {
1995
1996
  id: data.id,
1996
1997
  user_id: data.user_id,
@@ -1998,6 +1999,23 @@ var RLSTestContext = class {
1998
1999
  role_slug: data.role_slug
1999
2000
  };
2000
2001
  }
2002
+ /**
2003
+ * Assign a system role to a membership via org_rol_assignments.
2004
+ * After the 2026-04-25 auth refactor, RLS policies read `effective_permissions[]`
2005
+ * (materialized by trigger from org_rol_assignments → org_rol_grants).
2006
+ * Without this assignment, role_slug is informational only and the membership
2007
+ * has zero permissions, causing all has_org_permission() checks to deny.
2008
+ */
2009
+ async assignSystemRole(membershipId, slug) {
2010
+ const { data: roleDef, error: roleErr } = await this.adminClient.from("org_rol_definitions").select("id").is("organization_id", null).eq("slug", slug).single();
2011
+ if (roleErr || !roleDef) {
2012
+ throw new Error(`Failed to look up system role '${slug}': ${roleErr?.message ?? "not found"}`);
2013
+ }
2014
+ const { error: assignErr } = await this.adminClient.from("org_rol_assignments").insert({ membership_id: membershipId, role_id: roleDef.id });
2015
+ if (assignErr) {
2016
+ throw new Error(`Failed to assign system role '${slug}' to membership: ${assignErr.message}`);
2017
+ }
2018
+ }
2001
2019
  /**
2002
2020
  * Create a pre-provisioned organization membership (without WorkOS membership ID)
2003
2021
  * Used for testing invitation flows where memberships are created before user accepts
@@ -2016,6 +2034,7 @@ var RLSTestContext = class {
2016
2034
  throw new Error(`Failed to create pre-provisioned membership: ${error.message}`);
2017
2035
  }
2018
2036
  this.createdIds.memberships.push(data.id);
2037
+ await this.assignSystemRole(data.id, role);
2019
2038
  return {
2020
2039
  id: data.id,
2021
2040
  user_id: data.user_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/core",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "description": "Minimal shared constants across Elevasis monorepo",
6
6
  "sideEffects": false,
@@ -1,216 +1,217 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { encryptCredential, decryptCredential } from '../server/encryption'
3
-
4
- // Note: SECRETS_ENCRYPTION_KEY is set in vitest.config.ts env section
5
- // The encryption module reads it at load time
6
-
7
- describe('Credential Encryption', () =>{
8
-
9
- describe('encryptCredential', () => {
10
- it('encrypts plaintext to base64-encoded JSON string', () => {
11
- const plaintext = 'my-secret-api-key'
12
- const encrypted = encryptCredential(plaintext)
13
-
14
- // Should be a valid JSON string
15
- expect(() => JSON.parse(encrypted)).not.toThrow()
16
-
17
- const parsed = JSON.parse(encrypted)
18
- expect(parsed).toHaveProperty('iv')
19
- expect(parsed).toHaveProperty('authTag')
20
- expect(parsed).toHaveProperty('data')
21
- })
22
-
23
- it('produces different ciphertext for same input (random IV)', () => {
24
- const plaintext = 'same-secret'
25
-
26
- const encrypted1 = encryptCredential(plaintext)
27
- const encrypted2 = encryptCredential(plaintext)
28
-
29
- // Different IVs mean different ciphertext
30
- expect(encrypted1).not.toBe(encrypted2)
31
-
32
- // But both decrypt to same plaintext
33
- expect(decryptCredential(encrypted1)).toBe(plaintext)
34
- expect(decryptCredential(encrypted2)).toBe(plaintext)
35
- })
36
-
37
- it('handles empty string', () => {
38
- const plaintext = ''
39
- const encrypted = encryptCredential(plaintext)
40
- const decrypted = decryptCredential(encrypted)
41
-
42
- expect(decrypted).toBe('')
43
- })
44
-
45
- it('handles long strings', () => {
46
- const plaintext = 'x'.repeat(10000)
47
- const encrypted = encryptCredential(plaintext)
48
- const decrypted = decryptCredential(encrypted)
49
-
50
- expect(decrypted).toBe(plaintext)
51
- })
52
-
53
- it('handles special characters and unicode', () => {
54
- const plaintext = 'Hello 世界! 🔐 Special chars: \n\t\r"\'\\`'
55
- const encrypted = encryptCredential(plaintext)
56
- const decrypted = decryptCredential(encrypted)
57
-
58
- expect(decrypted).toBe(plaintext)
59
- })
60
-
61
- it('validates key is configured at module load', () => {
62
- // We can't test missing key dynamically since MASTER_KEY is set at module load
63
- // Instead, verify the key is set and encryption works
64
- const encrypted = encryptCredential('test')
65
- expect(encrypted).toBeDefined()
66
- })
67
- })
68
-
69
- describe('decryptCredential', () => {
70
- it('decrypts ciphertext back to original plaintext', () => {
71
- const plaintext = 'my-secret-api-key'
72
- const encrypted = encryptCredential(plaintext)
73
- const decrypted = decryptCredential(encrypted)
74
-
75
- expect(decrypted).toBe(plaintext)
76
- })
77
-
78
- it('successfully decrypts with configured key', () => {
79
- const encrypted = encryptCredential('test')
80
- const decrypted = decryptCredential(encrypted)
81
-
82
- expect(decrypted).toBe('test')
83
- })
84
-
85
- it('throws error on malformed JSON', () => {
86
- expect(() => decryptCredential('not-json')).toThrow()
87
- })
88
-
89
- it('throws error on missing fields', () => {
90
- const invalidData = JSON.stringify({ iv: 'abc', authTag: 'def' }) // Missing 'data'
91
-
92
- expect(() => decryptCredential(invalidData)).toThrow()
93
- })
94
-
95
- it('throws error on tampered ciphertext', () => {
96
- const encrypted = encryptCredential('secret')
97
- const parsed = JSON.parse(encrypted)
98
-
99
- // Tamper with the data
100
- parsed.data = 'tampered-data'
101
- const tampered = JSON.stringify(parsed)
102
-
103
- expect(() => decryptCredential(tampered)).toThrow()
104
- })
105
-
106
- it('throws error on tampered auth tag', () => {
107
- const encrypted = encryptCredential('secret')
108
- const parsed = JSON.parse(encrypted)
109
-
110
- // Tamper with the auth tag
111
- parsed.authTag = 'AAAAAAAAAAAAAAAAAAAAAA=='
112
- const tampered = JSON.stringify(parsed)
113
-
114
- expect(() => decryptCredential(tampered)).toThrow()
115
- })
116
-
117
- it('detects tampering via auth tag (GCM mode)', () => {
118
- const encrypted = encryptCredential('secret')
119
- const parsed = JSON.parse(encrypted)
120
-
121
- // Tamper with just one byte of data
122
- const dataBuffer = Buffer.from(parsed.data, 'base64')
123
- dataBuffer[0] ^= 0x01 // Flip one bit
124
- parsed.data = dataBuffer.toString('base64')
125
-
126
- const tampered = JSON.stringify(parsed)
127
-
128
- // GCM should detect the tampering and throw
129
- expect(() => decryptCredential(tampered)).toThrow()
130
- })
131
-
132
- it('throws error on invalid base64 encoding', () => {
133
- const invalidData = JSON.stringify({
134
- iv: 'not-valid-base64!!!',
135
- authTag: 'AAAA',
136
- data: 'BBBB'
137
- })
138
-
139
- expect(() => decryptCredential(invalidData)).toThrow()
140
- })
141
- })
142
-
143
- describe('Round-trip encryption/decryption', () => {
144
- it('preserves API key format', () => {
145
- const apiKey = 'sk_test_1234567890abcdefghijklmnopqrstuvwxyz'
146
- const encrypted = encryptCredential(apiKey)
147
- const decrypted = decryptCredential(encrypted)
148
-
149
- expect(decrypted).toBe(apiKey)
150
- })
151
-
152
- it('preserves database connection strings', () => {
153
- const connString = 'postgresql://user:password@localhost:5432/database?sslmode=require'
154
- const encrypted = encryptCredential(connString)
155
- const decrypted = decryptCredential(encrypted)
156
-
157
- expect(decrypted).toBe(connString)
158
- })
159
-
160
- it('preserves JSON credentials', () => {
161
- const jsonCreds = JSON.stringify({
162
- type: 'service_account',
163
- project_id: 'my-project',
164
- private_key: '-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n'
165
- })
166
-
167
- const encrypted = encryptCredential(jsonCreds)
168
- const decrypted = decryptCredential(encrypted)
169
-
170
- expect(decrypted).toBe(jsonCreds)
171
- expect(JSON.parse(decrypted)).toEqual(JSON.parse(jsonCreds))
172
- })
173
- })
174
-
175
- describe('Security properties', () => {
176
- it('uses AES-256-GCM (authenticated encryption)', () => {
177
- const encrypted = encryptCredential('test')
178
- const parsed = JSON.parse(encrypted)
179
-
180
- // Auth tag should be present (GCM mode)
181
- expect(parsed.authTag).toBeDefined()
182
- expect(parsed.authTag.length).toBeGreaterThan(0)
183
- })
184
-
185
- it('uses random IV for each encryption', () => {
186
- const ivs = new Set()
187
-
188
- for (let i = 0; i < 100; i++) {
189
- const encrypted = encryptCredential('test')
190
- const parsed = JSON.parse(encrypted)
191
- ivs.add(parsed.iv)
192
- }
193
-
194
- // All IVs should be unique
195
- expect(ivs.size).toBe(100)
196
- })
197
-
198
- it('IV is 16 bytes (128 bits)', () => {
199
- const encrypted = encryptCredential('test')
200
- const parsed = JSON.parse(encrypted)
201
-
202
- // Base64-encoded 16 bytes = 24 characters
203
- const ivBuffer = Buffer.from(parsed.iv, 'base64')
204
- expect(ivBuffer.length).toBe(16)
205
- })
206
-
207
- it('auth tag is 16 bytes (128 bits)', () => {
208
- const encrypted = encryptCredential('test')
209
- const parsed = JSON.parse(encrypted)
210
-
211
- // Base64-encoded 16 bytes = 24 characters
212
- const authTagBuffer = Buffer.from(parsed.authTag, 'base64')
213
- expect(authTagBuffer.length).toBe(16)
214
- })
215
- })
216
- })
1
+ import crypto from 'crypto'
2
+ import { describe, it, expect, beforeAll } from 'vitest'
3
+ import { encryptCredential, decryptCredential, setKek, CURRENT_KEY_ID } from '../server/encryption'
4
+
5
+ beforeAll(() => {
6
+ setKek(CURRENT_KEY_ID, crypto.randomBytes(32))
7
+ })
8
+
9
+ describe('Credential Encryption', () => {
10
+ describe('encryptCredential', () => {
11
+ it('encrypts plaintext to base64-encoded JSON string', () => {
12
+ const plaintext = 'my-secret-api-key'
13
+ const encrypted = encryptCredential(plaintext)
14
+
15
+ // Should be a valid JSON string
16
+ expect(() => JSON.parse(encrypted)).not.toThrow()
17
+
18
+ const parsed = JSON.parse(encrypted)
19
+ expect(parsed).toHaveProperty('iv')
20
+ expect(parsed).toHaveProperty('authTag')
21
+ expect(parsed).toHaveProperty('data')
22
+ })
23
+
24
+ it('produces different ciphertext for same input (random IV)', () => {
25
+ const plaintext = 'same-secret'
26
+
27
+ const encrypted1 = encryptCredential(plaintext)
28
+ const encrypted2 = encryptCredential(plaintext)
29
+
30
+ // Different IVs mean different ciphertext
31
+ expect(encrypted1).not.toBe(encrypted2)
32
+
33
+ // But both decrypt to same plaintext
34
+ expect(decryptCredential(encrypted1)).toBe(plaintext)
35
+ expect(decryptCredential(encrypted2)).toBe(plaintext)
36
+ })
37
+
38
+ it('handles empty string', () => {
39
+ const plaintext = ''
40
+ const encrypted = encryptCredential(plaintext)
41
+ const decrypted = decryptCredential(encrypted)
42
+
43
+ expect(decrypted).toBe('')
44
+ })
45
+
46
+ it('handles long strings', () => {
47
+ const plaintext = 'x'.repeat(10000)
48
+ const encrypted = encryptCredential(plaintext)
49
+ const decrypted = decryptCredential(encrypted)
50
+
51
+ expect(decrypted).toBe(plaintext)
52
+ })
53
+
54
+ it('handles special characters and unicode', () => {
55
+ const plaintext = 'Hello 世界! 🔐 Special chars: \n\t\r"\'\\`'
56
+ const encrypted = encryptCredential(plaintext)
57
+ const decrypted = decryptCredential(encrypted)
58
+
59
+ expect(decrypted).toBe(plaintext)
60
+ })
61
+
62
+ it('validates key is configured at module load', () => {
63
+ // We can't test missing key dynamically since MASTER_KEY is set at module load
64
+ // Instead, verify the key is set and encryption works
65
+ const encrypted = encryptCredential('test')
66
+ expect(encrypted).toBeDefined()
67
+ })
68
+ })
69
+
70
+ describe('decryptCredential', () => {
71
+ it('decrypts ciphertext back to original plaintext', () => {
72
+ const plaintext = 'my-secret-api-key'
73
+ const encrypted = encryptCredential(plaintext)
74
+ const decrypted = decryptCredential(encrypted)
75
+
76
+ expect(decrypted).toBe(plaintext)
77
+ })
78
+
79
+ it('successfully decrypts with configured key', () => {
80
+ const encrypted = encryptCredential('test')
81
+ const decrypted = decryptCredential(encrypted)
82
+
83
+ expect(decrypted).toBe('test')
84
+ })
85
+
86
+ it('throws error on malformed JSON', () => {
87
+ expect(() => decryptCredential('not-json')).toThrow()
88
+ })
89
+
90
+ it('throws error on missing fields', () => {
91
+ const invalidData = JSON.stringify({ iv: 'abc', authTag: 'def' }) // Missing 'data'
92
+
93
+ expect(() => decryptCredential(invalidData)).toThrow()
94
+ })
95
+
96
+ it('throws error on tampered ciphertext', () => {
97
+ const encrypted = encryptCredential('secret')
98
+ const parsed = JSON.parse(encrypted)
99
+
100
+ // Tamper with the data
101
+ parsed.data = 'tampered-data'
102
+ const tampered = JSON.stringify(parsed)
103
+
104
+ expect(() => decryptCredential(tampered)).toThrow()
105
+ })
106
+
107
+ it('throws error on tampered auth tag', () => {
108
+ const encrypted = encryptCredential('secret')
109
+ const parsed = JSON.parse(encrypted)
110
+
111
+ // Tamper with the auth tag
112
+ parsed.authTag = 'AAAAAAAAAAAAAAAAAAAAAA=='
113
+ const tampered = JSON.stringify(parsed)
114
+
115
+ expect(() => decryptCredential(tampered)).toThrow()
116
+ })
117
+
118
+ it('detects tampering via auth tag (GCM mode)', () => {
119
+ const encrypted = encryptCredential('secret')
120
+ const parsed = JSON.parse(encrypted)
121
+
122
+ // Tamper with just one byte of data
123
+ const dataBuffer = Buffer.from(parsed.data, 'base64')
124
+ dataBuffer[0] ^= 0x01 // Flip one bit
125
+ parsed.data = dataBuffer.toString('base64')
126
+
127
+ const tampered = JSON.stringify(parsed)
128
+
129
+ // GCM should detect the tampering and throw
130
+ expect(() => decryptCredential(tampered)).toThrow()
131
+ })
132
+
133
+ it('throws error on invalid base64 encoding', () => {
134
+ const invalidData = JSON.stringify({
135
+ iv: 'not-valid-base64!!!',
136
+ authTag: 'AAAA',
137
+ data: 'BBBB'
138
+ })
139
+
140
+ expect(() => decryptCredential(invalidData)).toThrow()
141
+ })
142
+ })
143
+
144
+ describe('Round-trip encryption/decryption', () => {
145
+ it('preserves API key format', () => {
146
+ const apiKey = 'sk_test_1234567890abcdefghijklmnopqrstuvwxyz'
147
+ const encrypted = encryptCredential(apiKey)
148
+ const decrypted = decryptCredential(encrypted)
149
+
150
+ expect(decrypted).toBe(apiKey)
151
+ })
152
+
153
+ it('preserves database connection strings', () => {
154
+ const connString = 'postgresql://user:password@localhost:5432/database?sslmode=require'
155
+ const encrypted = encryptCredential(connString)
156
+ const decrypted = decryptCredential(encrypted)
157
+
158
+ expect(decrypted).toBe(connString)
159
+ })
160
+
161
+ it('preserves JSON credentials', () => {
162
+ const jsonCreds = JSON.stringify({
163
+ type: 'service_account',
164
+ project_id: 'my-project',
165
+ private_key: '-----BEGIN PRIVATE KEY-----\nMIIE...\n-----END PRIVATE KEY-----\n'
166
+ })
167
+
168
+ const encrypted = encryptCredential(jsonCreds)
169
+ const decrypted = decryptCredential(encrypted)
170
+
171
+ expect(decrypted).toBe(jsonCreds)
172
+ expect(JSON.parse(decrypted)).toEqual(JSON.parse(jsonCreds))
173
+ })
174
+ })
175
+
176
+ describe('Security properties', () => {
177
+ it('uses AES-256-GCM (authenticated encryption)', () => {
178
+ const encrypted = encryptCredential('test')
179
+ const parsed = JSON.parse(encrypted)
180
+
181
+ // Auth tag should be present (GCM mode)
182
+ expect(parsed.authTag).toBeDefined()
183
+ expect(parsed.authTag.length).toBeGreaterThan(0)
184
+ })
185
+
186
+ it('uses random IV for each encryption', () => {
187
+ const ivs = new Set()
188
+
189
+ for (let i = 0; i < 100; i++) {
190
+ const encrypted = encryptCredential('test')
191
+ const parsed = JSON.parse(encrypted)
192
+ ivs.add(parsed.iv)
193
+ }
194
+
195
+ // All IVs should be unique
196
+ expect(ivs.size).toBe(100)
197
+ })
198
+
199
+ it('IV is 16 bytes (128 bits)', () => {
200
+ const encrypted = encryptCredential('test')
201
+ const parsed = JSON.parse(encrypted)
202
+
203
+ // Base64-encoded 16 bytes = 24 characters
204
+ const ivBuffer = Buffer.from(parsed.iv, 'base64')
205
+ expect(ivBuffer.length).toBe(16)
206
+ })
207
+
208
+ it('auth tag is 16 bytes (128 bits)', () => {
209
+ const encrypted = encryptCredential('test')
210
+ const parsed = JSON.parse(encrypted)
211
+
212
+ // Base64-encoded 16 bytes = 24 characters
213
+ const authTagBuffer = Buffer.from(parsed.authTag, 'base64')
214
+ expect(authTagBuffer.length).toBe(16)
215
+ })
216
+ })
217
+ })
@@ -5,10 +5,10 @@ const ALGORITHM = 'aes-256-gcm'
5
5
  // keyId stamped on all newly encrypted ciphertexts.
6
6
  export const CURRENT_KEY_ID = 'platform-v1'
7
7
 
8
- // Implicit keyId for pre-Vault ciphertext rows that were encrypted before this
9
- // field existed. The kek-loader registers the legacy SECRETS_ENCRYPTION_KEY env
10
- // value under this id during the migration window so existing rows decrypt
11
- // until the re-encryption script (B4) restamps them with CURRENT_KEY_ID.
8
+ // Implicit keyId for pre-Vault ciphertext rows that lack a keyId field. All
9
+ // known rows were restamped with CURRENT_KEY_ID during the re-encryption
10
+ // migration; this constant remains only so any unexpected legacy blob produces
11
+ // a clear "KEK not loaded" error rather than a silent default-key decrypt.
12
12
  export const LEGACY_KEY_ID = 'platform-v0-legacy'
13
13
 
14
14
  interface EncryptedData {
@@ -32,21 +32,7 @@ export function clearKeks(): void {
32
32
  }
33
33
 
34
34
  function resolveKek(keyId: string): Buffer | undefined {
35
- const cached = kekMap.get(keyId)
36
- if (cached) return cached
37
-
38
- // Non-production fallback: bootstrap from SECRETS_ENCRYPTION_KEY so tests and
39
- // local dev keep working without an explicit setKek() boot step. Production
40
- // must register KEKs via the kek-loader; the env var is removed in Wave B5.
41
- if (process.env.NODE_ENV !== 'production' && process.env.SECRETS_ENCRYPTION_KEY) {
42
- const envKey = Buffer.from(process.env.SECRETS_ENCRYPTION_KEY, 'hex')
43
- if (envKey.length === 32) {
44
- kekMap.set(keyId, envKey)
45
- return envKey
46
- }
47
- }
48
-
49
- return undefined
35
+ return kekMap.get(keyId)
50
36
  }
51
37
 
52
38
  export function encryptCredential(plaintext: string): string {