@nuxt-customer-portal/core 0.10.2 → 0.11.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @nuxt-customer-portal/core
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b604141: Return actionable encryption configuration errors when saving a portal email API key, display the explanation in email settings, and log server failures through Nitro with secret-safe structured diagnostics, including database migration and connectivity hints.
|
|
8
|
+
|
|
9
|
+
## 0.10.3
|
|
10
|
+
|
|
3
11
|
## 0.10.2
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
package/package.json
CHANGED
package/portal.manifest.mjs
CHANGED
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
decryptLegacyDotSecret,
|
|
17
17
|
decryptPortalSecret,
|
|
18
18
|
encryptPortalSecret,
|
|
19
|
-
isPortalSecretCiphertext
|
|
19
|
+
isPortalSecretCiphertext,
|
|
20
|
+
PortalEncryptionConfigurationError
|
|
20
21
|
} from './portal-encryption'
|
|
21
22
|
|
|
22
23
|
export type PortalEmailTextOverrides = Record<string, Partial<PortalEmailText>>
|
|
@@ -159,7 +160,21 @@ const emailEncryption = {
|
|
|
159
160
|
overrides: [{ env: 'PORTAL_EMAIL_ENCRYPTION_KEY', format: 'sha256' as const }]
|
|
160
161
|
}
|
|
161
162
|
|
|
162
|
-
export const encryptPortalEmailSecret = (value: string) =>
|
|
163
|
+
export const encryptPortalEmailSecret = (value: string) => {
|
|
164
|
+
try {
|
|
165
|
+
return encryptPortalSecret(value, emailEncryption)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof PortalEncryptionConfigurationError) {
|
|
168
|
+
throw createError({
|
|
169
|
+
statusCode: 503,
|
|
170
|
+
statusMessage: 'Email credential encryption is not configured correctly',
|
|
171
|
+
data: { code: 'EMAIL_ENCRYPTION_CONFIGURATION', message: error.message },
|
|
172
|
+
cause: error
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
throw error
|
|
176
|
+
}
|
|
177
|
+
}
|
|
163
178
|
|
|
164
179
|
export const decryptPortalEmailSecret = (value: string) => {
|
|
165
180
|
try {
|
|
@@ -13,14 +13,18 @@ export type PortalEncryptionOptions = {
|
|
|
13
13
|
const VERSION = 'v2'
|
|
14
14
|
const ROOT_KEY_ENV = 'PORTAL_ENCRYPTION_KEY'
|
|
15
15
|
|
|
16
|
+
export class PortalEncryptionConfigurationError extends Error {
|
|
17
|
+
override name = 'PortalEncryptionConfigurationError'
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
const decodeBase64Key = (value: string, name: string) => {
|
|
17
21
|
const normalized = value.trim()
|
|
18
22
|
if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(normalized)) {
|
|
19
|
-
throw new
|
|
23
|
+
throw new PortalEncryptionConfigurationError(`${name} must be a base64-encoded 32-byte key`)
|
|
20
24
|
}
|
|
21
25
|
const key = Buffer.from(normalized, normalized.includes('-') || normalized.includes('_') ? 'base64url' : 'base64')
|
|
22
26
|
if (key.length !== 32) {
|
|
23
|
-
throw new
|
|
27
|
+
throw new PortalEncryptionConfigurationError(`${name} must be a base64-encoded 32-byte key`)
|
|
24
28
|
}
|
|
25
29
|
return key
|
|
26
30
|
}
|
|
@@ -58,7 +62,7 @@ const resolvePortalEncryptionMaterial = ({ purpose, overrides = [] }: PortalEncr
|
|
|
58
62
|
|
|
59
63
|
const rootValue = process.env.PORTAL_ENCRYPTION_KEY
|
|
60
64
|
if (!rootValue) {
|
|
61
|
-
throw new
|
|
65
|
+
throw new PortalEncryptionConfigurationError(missingKeyMessage(overrides))
|
|
62
66
|
}
|
|
63
67
|
const rootKey = decodeBase64Key(rootValue, ROOT_KEY_ENV)
|
|
64
68
|
return {
|
|
@@ -74,7 +78,7 @@ const materialForSource = (source: string, options: PortalEncryptionOptions) =>
|
|
|
74
78
|
if (source === ROOT_KEY_ENV) {
|
|
75
79
|
const rootValue = process.env.PORTAL_ENCRYPTION_KEY
|
|
76
80
|
if (!rootValue) {
|
|
77
|
-
throw new
|
|
81
|
+
throw new PortalEncryptionConfigurationError(`Configure ${ROOT_KEY_ENV} to decrypt this credential`)
|
|
78
82
|
}
|
|
79
83
|
const rootKey = decodeBase64Key(rootValue, ROOT_KEY_ENV)
|
|
80
84
|
return Buffer.from(hkdfSync('sha256', rootKey, Buffer.alloc(0), `nuxt-customer-portal:${options.purpose}`, 32))
|
|
@@ -85,7 +89,7 @@ const materialForSource = (source: string, options: PortalEncryptionOptions) =>
|
|
|
85
89
|
}
|
|
86
90
|
const key = overrideKey(override)
|
|
87
91
|
if (!key) {
|
|
88
|
-
throw new
|
|
92
|
+
throw new PortalEncryptionConfigurationError(`Configure ${source} to decrypt this credential`)
|
|
89
93
|
}
|
|
90
94
|
return key
|
|
91
95
|
}
|
|
@@ -137,7 +141,9 @@ const requireLegacyOverrideKey = (overrides: PortalEncryptionOverride[]) => {
|
|
|
137
141
|
if (material) {
|
|
138
142
|
return material.key
|
|
139
143
|
}
|
|
140
|
-
throw new
|
|
144
|
+
throw new PortalEncryptionConfigurationError(
|
|
145
|
+
`Retain ${overrides.map(({ env }) => env).join(' or ')} to decrypt legacy credentials`
|
|
146
|
+
)
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
export const decryptLegacyDotSecret = (value: string, overrides: PortalEncryptionOverride[]) => {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { H3Event } from 'h3'
|
|
2
|
+
import { PortalEncryptionConfigurationError } from './portal-encryption'
|
|
3
|
+
|
|
4
|
+
const diagnostics: Record<string, string> = {
|
|
5
|
+
'42P01': 'Database table is missing; check package migrations',
|
|
6
|
+
'42703': 'Database column is missing; check package migrations',
|
|
7
|
+
'23503': 'Database foreign-key constraint failed',
|
|
8
|
+
'23505': 'Database unique constraint failed',
|
|
9
|
+
'42501': 'Database permissions are insufficient',
|
|
10
|
+
'28P01': 'Database authentication failed',
|
|
11
|
+
ECONNREFUSED: 'A required service refused the connection',
|
|
12
|
+
ETIMEDOUT: 'A required service timed out',
|
|
13
|
+
ENOTFOUND: 'A required service hostname could not be resolved'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const record = (value: unknown): Record<string, unknown> =>
|
|
17
|
+
value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
|
|
18
|
+
|
|
19
|
+
const diagnostic = (error: unknown) => {
|
|
20
|
+
let current = error
|
|
21
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
22
|
+
if (current instanceof PortalEncryptionConfigurationError) {
|
|
23
|
+
return { code: 'PORTAL_ENCRYPTION_CONFIGURATION', message: current.message }
|
|
24
|
+
}
|
|
25
|
+
const { code, cause } = record(current)
|
|
26
|
+
if (typeof code === 'string' && Object.hasOwn(diagnostics, code)) {
|
|
27
|
+
return { code, message: diagnostics[code] }
|
|
28
|
+
}
|
|
29
|
+
current = cause
|
|
30
|
+
}
|
|
31
|
+
return { code: 'SERVER_ERROR', message: 'Unexpected server error' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const logPortalServerError = (
|
|
35
|
+
error: unknown,
|
|
36
|
+
event?: H3Event,
|
|
37
|
+
write: (message: string) => void = console.error
|
|
38
|
+
) => {
|
|
39
|
+
const value = record(error).statusCode
|
|
40
|
+
const statusCode = typeof value === 'number' && value >= 400 && value <= 599 ? value : 500
|
|
41
|
+
if (statusCode < 500) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
write(
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
event: 'portal.server_error',
|
|
47
|
+
method: event?.method ?? null,
|
|
48
|
+
path: event?.path.split('?')[0] ?? null,
|
|
49
|
+
statusCode,
|
|
50
|
+
...diagnostic(error)
|
|
51
|
+
})
|
|
52
|
+
)
|
|
53
|
+
}
|