@better-forge/vault 0.1.0 → 0.1.1
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/LICENSE +21 -21
- package/README.md +5 -5
- package/dist/migrations.js +21 -21
- package/package.json +56 -56
- package/src/createVault.ts +45 -45
- package/src/hashicorp.ts +466 -466
- package/src/index.ts +9 -9
- package/src/migrations.ts +29 -29
- package/src/module.ts +6 -6
- package/src/resolveSecret.ts +96 -96
- package/src/sealed.ts +38 -38
- package/src/secretBox.ts +72 -72
- package/src/secretFields.ts +177 -177
- package/src/tables.ts +23 -23
- package/src/vaultRoutes.ts +422 -422
- package/src/vaultUse.ts +41 -41
package/src/secretFields.ts
CHANGED
|
@@ -1,177 +1,177 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Which parts of a configuration are secrets.
|
|
5
|
-
*
|
|
6
|
-
* Secrets never leave the API as stored: reads get a mask, and a write that sends the mask back means "keep the
|
|
7
|
-
* stored value". The same table drives the config export, so a new secret field is declared in exactly one place.
|
|
8
|
-
* Values read from a vault are references, not secrets, and are not listed here.
|
|
9
|
-
*
|
|
10
|
-
* The table is grouped by type for readability, but masking and restoring look at every secret field of the kind
|
|
11
|
-
* whatever the object's type. A config can hold leftovers of another type (the type changed, or an API client sent
|
|
12
|
-
* extra keys), and those must not read back in the clear.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { Sealer } from './sealed.js'
|
|
16
|
-
|
|
17
|
-
export const SECRET_MASK = '••••••••'
|
|
18
|
-
|
|
19
|
-
/** Three bullets in a row are never part of a real secret; they are a mask, or a mask somebody edited by hand. */
|
|
20
|
-
const LOOKS_MASKED = /•{3}/
|
|
21
|
-
|
|
22
|
-
export type SecretField =
|
|
23
|
-
/** A string at `path`; `tail` also shows its last 4 characters (long tokens only) so it can be recognised. */
|
|
24
|
-
| { path: readonly string[]; tail?: true }
|
|
25
|
-
/** Every entry of the object at `mapAt` whose key names a credential (Authorization, X-Api-Key, ...). */
|
|
26
|
-
| { mapAt: readonly string[] }
|
|
27
|
-
|
|
28
|
-
/** Credential-looking header names. Wide on purpose: masking `X-Idempotency-Key` costs nothing, leaking `X-Auth` does. */
|
|
29
|
-
export const SENSITIVE_HEADER = /auth|token|secret|key|pass|pwd|credential|cookie|session|signature/i
|
|
30
|
-
|
|
31
|
-
export const uniqueSecretFields = (fields: readonly SecretField[]): SecretField[] => {
|
|
32
|
-
const seen = new Set<string>()
|
|
33
|
-
return fields.filter((field) => {
|
|
34
|
-
const id = 'mapAt' in field ? `map:${field.mapAt.join('.')}` : `path:${field.path.join('.')}`
|
|
35
|
-
return seen.has(id) ? false : (seen.add(id), true)
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type Json = Record<string, unknown>
|
|
40
|
-
|
|
41
|
-
const isObject = (value: unknown): value is Json => !!value && typeof value === 'object' && !Array.isArray(value)
|
|
42
|
-
|
|
43
|
-
function valueAt(root: unknown, path: readonly string[]): unknown {
|
|
44
|
-
let current = root
|
|
45
|
-
for (const segment of path) {
|
|
46
|
-
if (!isObject(current)) return undefined
|
|
47
|
-
current = current[segment]
|
|
48
|
-
}
|
|
49
|
-
return current
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** One concrete secret value: the object holding it and its key, plus the dotted name used in messages. */
|
|
53
|
-
interface Slot { holder: Json; key: string; label: string; tail: boolean }
|
|
54
|
-
|
|
55
|
-
/** `••••••••`, plus the last 4 characters of a long token so it can be told apart from another one. */
|
|
56
|
-
function mask(value: string, tail: boolean): string {
|
|
57
|
-
return `${SECRET_MASK}${tail && value.length >= 16 ? value.slice(-4) : ''}`
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export type RestoreResult = { config: unknown } | { error: string }
|
|
61
|
-
|
|
62
|
-
/** A switch that makes a destination less safe: a path whose value is `true`, or a path with the value that is the unsafe one (`verifyTls` off is `false`). */
|
|
63
|
-
export type LooseningSwitch = readonly string[] | { path: readonly string[]; value: unknown }
|
|
64
|
-
|
|
65
|
-
export function createSecretFields<Kind extends string>(kinds: Readonly<Record<Kind, {
|
|
66
|
-
fields: readonly SecretField[]
|
|
67
|
-
destinations: readonly (readonly string[])[]
|
|
68
|
-
/** Switches that make a destination less safe (certificate checks off): turning one on counts as a changed destination, turning it off does not. */
|
|
69
|
-
loosening?: readonly LooseningSwitch[]
|
|
70
|
-
}>>) {
|
|
71
|
-
function slotsIn(config: unknown, kind: Kind): Slot[] {
|
|
72
|
-
const slots: Slot[] = []
|
|
73
|
-
for (const field of kinds[kind].fields) {
|
|
74
|
-
if ('mapAt' in field) {
|
|
75
|
-
const holder = valueAt(config, field.mapAt)
|
|
76
|
-
if (!isObject(holder)) continue
|
|
77
|
-
for (const key of Object.keys(holder)) {
|
|
78
|
-
if (SENSITIVE_HEADER.test(key)) slots.push({ holder, key, label: `${field.mapAt.join('.')}.${key}`, tail: false })
|
|
79
|
-
}
|
|
80
|
-
} else {
|
|
81
|
-
const holder = valueAt(config, field.path.slice(0, -1))
|
|
82
|
-
const key = field.path[field.path.length - 1]!
|
|
83
|
-
if (isObject(holder) && key in holder) slots.push({ holder, key, label: field.path.join('.'), tail: field.tail === true })
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return slots
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** A copy of `config` with every secret replaced by its mask, whatever its type (a number is as secret as a string). */
|
|
90
|
-
function maskSecrets<T>(kind: Kind, config: T): T {
|
|
91
|
-
const copy = structuredClone(config)
|
|
92
|
-
for (const { holder, key, tail } of slotsIn(copy, kind)) {
|
|
93
|
-
const value = holder[key]
|
|
94
|
-
if (value !== null && value !== undefined && value !== '') holder[key] = mask(String(value), tail)
|
|
95
|
-
}
|
|
96
|
-
return copy
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Resolves the secrets of an incoming config against the stored one: a mask that matches the stored secret becomes
|
|
101
|
-
* that secret again. Refused instead of saved:
|
|
102
|
-
* - a mask, or something that looks like an edited mask, that matches nothing stored;
|
|
103
|
-
* - a secret that is not text;
|
|
104
|
-
* - a kept secret while the place it is sent to changed (URL, host, port), so it cannot be redirected.
|
|
105
|
-
* Pass `stored` as undefined for a new object (or one whose type changed), where any mask is a mistake.
|
|
106
|
-
*/
|
|
107
|
-
function restoreSecrets(kind: Kind, incoming: unknown, stored: unknown): RestoreResult {
|
|
108
|
-
const copy = structuredClone(incoming)
|
|
109
|
-
const storedSlots = new Map(slotsIn(stored, kind).map((slot) => [slot.label, slot]))
|
|
110
|
-
let kept = false
|
|
111
|
-
for (const { holder, key, label, tail } of slotsIn(copy, kind)) {
|
|
112
|
-
const value = holder[key]
|
|
113
|
-
if (value === null || value === undefined) continue
|
|
114
|
-
if (typeof value !== 'string') return { error: `${label} must be text` }
|
|
115
|
-
if (!LOOKS_MASKED.test(value)) continue
|
|
116
|
-
const previous = storedSlots.get(label)
|
|
117
|
-
const original = previous?.holder[previous.key]
|
|
118
|
-
if (typeof original === 'string' && original && value === mask(original, tail)) {
|
|
119
|
-
holder[key] = original
|
|
120
|
-
kept = true
|
|
121
|
-
} else {
|
|
122
|
-
return { error: `${label} is a masked placeholder that does not match the stored value; enter the full value to replace it` }
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (kept) {
|
|
126
|
-
const moved = changedDestination(kind, copy, stored)
|
|
127
|
-
if (moved) return { error: `${moved} changed, so the stored secrets cannot be kept; enter them again` }
|
|
128
|
-
}
|
|
129
|
-
return { config: copy }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** The first place a configuration is sent to (URL, host, port) that differs between `incoming` and `stored`, or null. */
|
|
133
|
-
function changedDestination(kind: Kind, incoming: unknown, stored: unknown): string | null {
|
|
134
|
-
for (const path of kinds[kind].destinations) {
|
|
135
|
-
// An empty value means the same as an absent one.
|
|
136
|
-
const normalised = (config: unknown) => valueAt(config, path) || null
|
|
137
|
-
if (JSON.stringify(normalised(incoming)) !== JSON.stringify(normalised(stored))) return path.join('.')
|
|
138
|
-
}
|
|
139
|
-
for (const entry of kinds[kind].loosening ?? []) {
|
|
140
|
-
const { path, value } = Array.isArray(entry) ? { path: entry as readonly string[], value: true } : entry as { path: readonly string[]; value: unknown }
|
|
141
|
-
if (valueAt(incoming, path) === value && valueAt(stored, path) !== value) return path.join('.')
|
|
142
|
-
}
|
|
143
|
-
return null
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** A copy of `config` with every secret that is text passed through `convert` (a vault reference or a number is not text and stays). */
|
|
147
|
-
function convertSecrets<T>(kind: Kind, config: T, convert: (value: string) => string): T {
|
|
148
|
-
const copy = structuredClone(config)
|
|
149
|
-
for (const { holder, key } of slotsIn(copy, kind)) {
|
|
150
|
-
const value = holder[key]
|
|
151
|
-
if (typeof value === 'string') holder[key] = convert(value)
|
|
152
|
-
}
|
|
153
|
-
return copy
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* The codec of a column that stores a configuration as JSON text: the secrets are encrypted on the way in and opened
|
|
158
|
-
* on the way out, so a copy of the database alone does not carry them. Text that is not JSON is passed through.
|
|
159
|
-
*/
|
|
160
|
-
function configCodec(kind: Kind, sealer: Pick<Sealer, 'seal' | 'open'>): Pick<Sealer, 'seal' | 'open'> {
|
|
161
|
-
const through = (convert: (value: string) => string) => (json: string): string => {
|
|
162
|
-
let parsed: unknown
|
|
163
|
-
try { parsed = JSON.parse(json) } catch { return json }
|
|
164
|
-
// A config without a secret is returned as it was written, not as JSON.stringify would write it.
|
|
165
|
-
let changed = false
|
|
166
|
-
const converted = convertSecrets(kind, parsed, (value) => {
|
|
167
|
-
const result = convert(value)
|
|
168
|
-
if (result !== value) changed = true
|
|
169
|
-
return result
|
|
170
|
-
})
|
|
171
|
-
return changed ? JSON.stringify(converted) : json
|
|
172
|
-
}
|
|
173
|
-
return { seal: through(sealer.seal), open: through(sealer.open) }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return { maskSecrets, restoreSecrets, changedDestination, configCodec }
|
|
177
|
-
}
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Which parts of a configuration are secrets.
|
|
5
|
+
*
|
|
6
|
+
* Secrets never leave the API as stored: reads get a mask, and a write that sends the mask back means "keep the
|
|
7
|
+
* stored value". The same table drives the config export, so a new secret field is declared in exactly one place.
|
|
8
|
+
* Values read from a vault are references, not secrets, and are not listed here.
|
|
9
|
+
*
|
|
10
|
+
* The table is grouped by type for readability, but masking and restoring look at every secret field of the kind
|
|
11
|
+
* whatever the object's type. A config can hold leftovers of another type (the type changed, or an API client sent
|
|
12
|
+
* extra keys), and those must not read back in the clear.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Sealer } from './sealed.js'
|
|
16
|
+
|
|
17
|
+
export const SECRET_MASK = '••••••••'
|
|
18
|
+
|
|
19
|
+
/** Three bullets in a row are never part of a real secret; they are a mask, or a mask somebody edited by hand. */
|
|
20
|
+
const LOOKS_MASKED = /•{3}/
|
|
21
|
+
|
|
22
|
+
export type SecretField =
|
|
23
|
+
/** A string at `path`; `tail` also shows its last 4 characters (long tokens only) so it can be recognised. */
|
|
24
|
+
| { path: readonly string[]; tail?: true }
|
|
25
|
+
/** Every entry of the object at `mapAt` whose key names a credential (Authorization, X-Api-Key, ...). */
|
|
26
|
+
| { mapAt: readonly string[] }
|
|
27
|
+
|
|
28
|
+
/** Credential-looking header names. Wide on purpose: masking `X-Idempotency-Key` costs nothing, leaking `X-Auth` does. */
|
|
29
|
+
export const SENSITIVE_HEADER = /auth|token|secret|key|pass|pwd|credential|cookie|session|signature/i
|
|
30
|
+
|
|
31
|
+
export const uniqueSecretFields = (fields: readonly SecretField[]): SecretField[] => {
|
|
32
|
+
const seen = new Set<string>()
|
|
33
|
+
return fields.filter((field) => {
|
|
34
|
+
const id = 'mapAt' in field ? `map:${field.mapAt.join('.')}` : `path:${field.path.join('.')}`
|
|
35
|
+
return seen.has(id) ? false : (seen.add(id), true)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type Json = Record<string, unknown>
|
|
40
|
+
|
|
41
|
+
const isObject = (value: unknown): value is Json => !!value && typeof value === 'object' && !Array.isArray(value)
|
|
42
|
+
|
|
43
|
+
function valueAt(root: unknown, path: readonly string[]): unknown {
|
|
44
|
+
let current = root
|
|
45
|
+
for (const segment of path) {
|
|
46
|
+
if (!isObject(current)) return undefined
|
|
47
|
+
current = current[segment]
|
|
48
|
+
}
|
|
49
|
+
return current
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One concrete secret value: the object holding it and its key, plus the dotted name used in messages. */
|
|
53
|
+
interface Slot { holder: Json; key: string; label: string; tail: boolean }
|
|
54
|
+
|
|
55
|
+
/** `••••••••`, plus the last 4 characters of a long token so it can be told apart from another one. */
|
|
56
|
+
function mask(value: string, tail: boolean): string {
|
|
57
|
+
return `${SECRET_MASK}${tail && value.length >= 16 ? value.slice(-4) : ''}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type RestoreResult = { config: unknown } | { error: string }
|
|
61
|
+
|
|
62
|
+
/** A switch that makes a destination less safe: a path whose value is `true`, or a path with the value that is the unsafe one (`verifyTls` off is `false`). */
|
|
63
|
+
export type LooseningSwitch = readonly string[] | { path: readonly string[]; value: unknown }
|
|
64
|
+
|
|
65
|
+
export function createSecretFields<Kind extends string>(kinds: Readonly<Record<Kind, {
|
|
66
|
+
fields: readonly SecretField[]
|
|
67
|
+
destinations: readonly (readonly string[])[]
|
|
68
|
+
/** Switches that make a destination less safe (certificate checks off): turning one on counts as a changed destination, turning it off does not. */
|
|
69
|
+
loosening?: readonly LooseningSwitch[]
|
|
70
|
+
}>>) {
|
|
71
|
+
function slotsIn(config: unknown, kind: Kind): Slot[] {
|
|
72
|
+
const slots: Slot[] = []
|
|
73
|
+
for (const field of kinds[kind].fields) {
|
|
74
|
+
if ('mapAt' in field) {
|
|
75
|
+
const holder = valueAt(config, field.mapAt)
|
|
76
|
+
if (!isObject(holder)) continue
|
|
77
|
+
for (const key of Object.keys(holder)) {
|
|
78
|
+
if (SENSITIVE_HEADER.test(key)) slots.push({ holder, key, label: `${field.mapAt.join('.')}.${key}`, tail: false })
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
const holder = valueAt(config, field.path.slice(0, -1))
|
|
82
|
+
const key = field.path[field.path.length - 1]!
|
|
83
|
+
if (isObject(holder) && key in holder) slots.push({ holder, key, label: field.path.join('.'), tail: field.tail === true })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return slots
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A copy of `config` with every secret replaced by its mask, whatever its type (a number is as secret as a string). */
|
|
90
|
+
function maskSecrets<T>(kind: Kind, config: T): T {
|
|
91
|
+
const copy = structuredClone(config)
|
|
92
|
+
for (const { holder, key, tail } of slotsIn(copy, kind)) {
|
|
93
|
+
const value = holder[key]
|
|
94
|
+
if (value !== null && value !== undefined && value !== '') holder[key] = mask(String(value), tail)
|
|
95
|
+
}
|
|
96
|
+
return copy
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolves the secrets of an incoming config against the stored one: a mask that matches the stored secret becomes
|
|
101
|
+
* that secret again. Refused instead of saved:
|
|
102
|
+
* - a mask, or something that looks like an edited mask, that matches nothing stored;
|
|
103
|
+
* - a secret that is not text;
|
|
104
|
+
* - a kept secret while the place it is sent to changed (URL, host, port), so it cannot be redirected.
|
|
105
|
+
* Pass `stored` as undefined for a new object (or one whose type changed), where any mask is a mistake.
|
|
106
|
+
*/
|
|
107
|
+
function restoreSecrets(kind: Kind, incoming: unknown, stored: unknown): RestoreResult {
|
|
108
|
+
const copy = structuredClone(incoming)
|
|
109
|
+
const storedSlots = new Map(slotsIn(stored, kind).map((slot) => [slot.label, slot]))
|
|
110
|
+
let kept = false
|
|
111
|
+
for (const { holder, key, label, tail } of slotsIn(copy, kind)) {
|
|
112
|
+
const value = holder[key]
|
|
113
|
+
if (value === null || value === undefined) continue
|
|
114
|
+
if (typeof value !== 'string') return { error: `${label} must be text` }
|
|
115
|
+
if (!LOOKS_MASKED.test(value)) continue
|
|
116
|
+
const previous = storedSlots.get(label)
|
|
117
|
+
const original = previous?.holder[previous.key]
|
|
118
|
+
if (typeof original === 'string' && original && value === mask(original, tail)) {
|
|
119
|
+
holder[key] = original
|
|
120
|
+
kept = true
|
|
121
|
+
} else {
|
|
122
|
+
return { error: `${label} is a masked placeholder that does not match the stored value; enter the full value to replace it` }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (kept) {
|
|
126
|
+
const moved = changedDestination(kind, copy, stored)
|
|
127
|
+
if (moved) return { error: `${moved} changed, so the stored secrets cannot be kept; enter them again` }
|
|
128
|
+
}
|
|
129
|
+
return { config: copy }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The first place a configuration is sent to (URL, host, port) that differs between `incoming` and `stored`, or null. */
|
|
133
|
+
function changedDestination(kind: Kind, incoming: unknown, stored: unknown): string | null {
|
|
134
|
+
for (const path of kinds[kind].destinations) {
|
|
135
|
+
// An empty value means the same as an absent one.
|
|
136
|
+
const normalised = (config: unknown) => valueAt(config, path) || null
|
|
137
|
+
if (JSON.stringify(normalised(incoming)) !== JSON.stringify(normalised(stored))) return path.join('.')
|
|
138
|
+
}
|
|
139
|
+
for (const entry of kinds[kind].loosening ?? []) {
|
|
140
|
+
const { path, value } = Array.isArray(entry) ? { path: entry as readonly string[], value: true } : entry as { path: readonly string[]; value: unknown }
|
|
141
|
+
if (valueAt(incoming, path) === value && valueAt(stored, path) !== value) return path.join('.')
|
|
142
|
+
}
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A copy of `config` with every secret that is text passed through `convert` (a vault reference or a number is not text and stays). */
|
|
147
|
+
function convertSecrets<T>(kind: Kind, config: T, convert: (value: string) => string): T {
|
|
148
|
+
const copy = structuredClone(config)
|
|
149
|
+
for (const { holder, key } of slotsIn(copy, kind)) {
|
|
150
|
+
const value = holder[key]
|
|
151
|
+
if (typeof value === 'string') holder[key] = convert(value)
|
|
152
|
+
}
|
|
153
|
+
return copy
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The codec of a column that stores a configuration as JSON text: the secrets are encrypted on the way in and opened
|
|
158
|
+
* on the way out, so a copy of the database alone does not carry them. Text that is not JSON is passed through.
|
|
159
|
+
*/
|
|
160
|
+
function configCodec(kind: Kind, sealer: Pick<Sealer, 'seal' | 'open'>): Pick<Sealer, 'seal' | 'open'> {
|
|
161
|
+
const through = (convert: (value: string) => string) => (json: string): string => {
|
|
162
|
+
let parsed: unknown
|
|
163
|
+
try { parsed = JSON.parse(json) } catch { return json }
|
|
164
|
+
// A config without a secret is returned as it was written, not as JSON.stringify would write it.
|
|
165
|
+
let changed = false
|
|
166
|
+
const converted = convertSecrets(kind, parsed, (value) => {
|
|
167
|
+
const result = convert(value)
|
|
168
|
+
if (result !== value) changed = true
|
|
169
|
+
return result
|
|
170
|
+
})
|
|
171
|
+
return changed ? JSON.stringify(converted) : json
|
|
172
|
+
}
|
|
173
|
+
return { seal: through(sealer.seal), open: through(sealer.open) }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { maskSecrets, restoreSecrets, changedDestination, configCodec }
|
|
177
|
+
}
|
package/src/tables.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
|
|
2
|
-
|
|
3
|
-
export const vaults = sqliteTable('vaults', {
|
|
4
|
-
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
5
|
-
name: text('name').notNull(),
|
|
6
|
-
type: text('type').notNull().default('local'), // 'local' | 'hashicorp'
|
|
7
|
-
description: text('description'),
|
|
8
|
-
createdAt: integer('created_at').notNull(),
|
|
9
|
-
updatedAt: integer('updated_at').notNull(),
|
|
10
|
-
/** Encrypted JSON connection settings of a 'hashicorp' vault; NULL for 'local'. Never sent to the browser. */
|
|
11
|
-
connectionConfig: text('connection_config'),
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
export const vaultSecrets = sqliteTable('vault_secrets', {
|
|
15
|
-
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
16
|
-
vaultId: integer('vault_id').notNull(),
|
|
17
|
-
name: text('name').notNull(),
|
|
18
|
-
type: text('type').notNull(), // 'userpass' | 'value' | 'json'
|
|
19
|
-
/** Local vault: the encrypted value. HashiCorp vault: the encrypted reference `{ path, key? }`. */
|
|
20
|
-
encryptedValue: text('encrypted_value').notNull(),
|
|
21
|
-
createdAt: integer('created_at').notNull(),
|
|
22
|
-
updatedAt: integer('updated_at').notNull(),
|
|
23
|
-
})
|
|
1
|
+
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
|
|
2
|
+
|
|
3
|
+
export const vaults = sqliteTable('vaults', {
|
|
4
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
5
|
+
name: text('name').notNull(),
|
|
6
|
+
type: text('type').notNull().default('local'), // 'local' | 'hashicorp'
|
|
7
|
+
description: text('description'),
|
|
8
|
+
createdAt: integer('created_at').notNull(),
|
|
9
|
+
updatedAt: integer('updated_at').notNull(),
|
|
10
|
+
/** Encrypted JSON connection settings of a 'hashicorp' vault; NULL for 'local'. Never sent to the browser. */
|
|
11
|
+
connectionConfig: text('connection_config'),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const vaultSecrets = sqliteTable('vault_secrets', {
|
|
15
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
16
|
+
vaultId: integer('vault_id').notNull(),
|
|
17
|
+
name: text('name').notNull(),
|
|
18
|
+
type: text('type').notNull(), // 'userpass' | 'value' | 'json'
|
|
19
|
+
/** Local vault: the encrypted value. HashiCorp vault: the encrypted reference `{ path, key? }`. */
|
|
20
|
+
encryptedValue: text('encrypted_value').notNull(),
|
|
21
|
+
createdAt: integer('created_at').notNull(),
|
|
22
|
+
updatedAt: integer('updated_at').notNull(),
|
|
23
|
+
})
|