@shipfox/api-secrets 2.0.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.
Files changed (288) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +116 -0
  5. package/LICENSE +21 -0
  6. package/README.md +207 -0
  7. package/dist/config.d.ts +8 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +38 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/core/crypto.d.ts +25 -0
  12. package/dist/core/crypto.d.ts.map +1 -0
  13. package/dist/core/crypto.js +85 -0
  14. package/dist/core/crypto.js.map +1 -0
  15. package/dist/core/dek-manager.d.ts +16 -0
  16. package/dist/core/dek-manager.d.ts.map +1 -0
  17. package/dist/core/dek-manager.js +85 -0
  18. package/dist/core/dek-manager.js.map +1 -0
  19. package/dist/core/errors.d.ts +53 -0
  20. package/dist/core/errors.d.ts.map +1 -0
  21. package/dist/core/errors.js +86 -0
  22. package/dist/core/errors.js.map +1 -0
  23. package/dist/core/fingerprint.d.ts +2 -0
  24. package/dist/core/fingerprint.d.ts.map +1 -0
  25. package/dist/core/fingerprint.js +9 -0
  26. package/dist/core/fingerprint.js.map +1 -0
  27. package/dist/core/index.d.ts +30 -0
  28. package/dist/core/index.d.ts.map +1 -0
  29. package/dist/core/index.js +97 -0
  30. package/dist/core/index.js.map +1 -0
  31. package/dist/core/key-provider.d.ts +17 -0
  32. package/dist/core/key-provider.d.ts.map +1 -0
  33. package/dist/core/key-provider.js +45 -0
  34. package/dist/core/key-provider.js.map +1 -0
  35. package/dist/core/local-secret-store.d.ts +19 -0
  36. package/dist/core/local-secret-store.d.ts.map +1 -0
  37. package/dist/core/local-secret-store.js +40 -0
  38. package/dist/core/local-secret-store.js.map +1 -0
  39. package/dist/core/management.d.ts +37 -0
  40. package/dist/core/management.d.ts.map +1 -0
  41. package/dist/core/management.js +403 -0
  42. package/dist/core/management.js.map +1 -0
  43. package/dist/core/rotate-kek.d.ts +10 -0
  44. package/dist/core/rotate-kek.d.ts.map +1 -0
  45. package/dist/core/rotate-kek.js +94 -0
  46. package/dist/core/rotate-kek.js.map +1 -0
  47. package/dist/core/secret-store.d.ts +33 -0
  48. package/dist/core/secret-store.d.ts.map +1 -0
  49. package/dist/core/secret-store.js +209 -0
  50. package/dist/core/secret-store.js.map +1 -0
  51. package/dist/core/store-resolver.d.ts +18 -0
  52. package/dist/core/store-resolver.d.ts.map +1 -0
  53. package/dist/core/store-resolver.js +10 -0
  54. package/dist/core/store-resolver.js.map +1 -0
  55. package/dist/core/store-validation.d.ts +11 -0
  56. package/dist/core/store-validation.d.ts.map +1 -0
  57. package/dist/core/store-validation.js +30 -0
  58. package/dist/core/store-validation.js.map +1 -0
  59. package/dist/core/value-cipher.d.ts +10 -0
  60. package/dist/core/value-cipher.d.ts.map +1 -0
  61. package/dist/core/value-cipher.js +22 -0
  62. package/dist/core/value-cipher.js.map +1 -0
  63. package/dist/core/variable-store.d.ts +24 -0
  64. package/dist/core/variable-store.d.ts.map +1 -0
  65. package/dist/core/variable-store.js +194 -0
  66. package/dist/core/variable-store.js.map +1 -0
  67. package/dist/db/cap.d.ts +8 -0
  68. package/dist/db/cap.d.ts.map +1 -0
  69. package/dist/db/cap.js +39 -0
  70. package/dist/db/cap.js.map +1 -0
  71. package/dist/db/data-keys.d.ts +24 -0
  72. package/dist/db/data-keys.d.ts.map +1 -0
  73. package/dist/db/data-keys.js +61 -0
  74. package/dist/db/data-keys.js.map +1 -0
  75. package/dist/db/db.d.ts +1254 -0
  76. package/dist/db/db.d.ts.map +1 -0
  77. package/dist/db/db.js +24 -0
  78. package/dist/db/db.js.map +1 -0
  79. package/dist/db/index.d.ts +13 -0
  80. package/dist/db/index.d.ts.map +1 -0
  81. package/dist/db/index.js +15 -0
  82. package/dist/db/index.js.map +1 -0
  83. package/dist/db/management.d.ts +50 -0
  84. package/dist/db/management.d.ts.map +1 -0
  85. package/dist/db/management.js +127 -0
  86. package/dist/db/management.js.map +1 -0
  87. package/dist/db/schema/common.d.ts +3 -0
  88. package/dist/db/schema/common.d.ts.map +1 -0
  89. package/dist/db/schema/common.js +8 -0
  90. package/dist/db/schema/common.js.map +1 -0
  91. package/dist/db/schema/data-keys.d.ts +103 -0
  92. package/dist/db/schema/data-keys.d.ts.map +1 -0
  93. package/dist/db/schema/data-keys.js +24 -0
  94. package/dist/db/schema/data-keys.js.map +1 -0
  95. package/dist/db/schema/outbox.d.ts +195 -0
  96. package/dist/db/schema/outbox.d.ts.map +1 -0
  97. package/dist/db/schema/outbox.js +5 -0
  98. package/dist/db/schema/outbox.js.map +1 -0
  99. package/dist/db/schema/values.d.ts +193 -0
  100. package/dist/db/schema/values.d.ts.map +1 -0
  101. package/dist/db/schema/values.js +43 -0
  102. package/dist/db/schema/values.js.map +1 -0
  103. package/dist/db/schema/variables.d.ts +175 -0
  104. package/dist/db/schema/variables.d.ts.map +1 -0
  105. package/dist/db/schema/variables.js +41 -0
  106. package/dist/db/schema/variables.js.map +1 -0
  107. package/dist/db/scope.d.ts +20 -0
  108. package/dist/db/scope.d.ts.map +1 -0
  109. package/dist/db/scope.js +19 -0
  110. package/dist/db/scope.js.map +1 -0
  111. package/dist/db/values.d.ts +33 -0
  112. package/dist/db/values.d.ts.map +1 -0
  113. package/dist/db/values.js +119 -0
  114. package/dist/db/values.js.map +1 -0
  115. package/dist/db/variables.d.ts +32 -0
  116. package/dist/db/variables.d.ts.map +1 -0
  117. package/dist/db/variables.js +117 -0
  118. package/dist/db/variables.js.map +1 -0
  119. package/dist/index.d.ts +4 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +28 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/metrics/index.d.ts +3 -0
  124. package/dist/metrics/index.d.ts.map +1 -0
  125. package/dist/metrics/index.js +4 -0
  126. package/dist/metrics/index.js.map +1 -0
  127. package/dist/metrics/instance.d.ts +39 -0
  128. package/dist/metrics/instance.d.ts.map +1 -0
  129. package/dist/metrics/instance.js +151 -0
  130. package/dist/metrics/instance.js.map +1 -0
  131. package/dist/metrics/service.d.ts +2 -0
  132. package/dist/metrics/service.d.ts.map +1 -0
  133. package/dist/metrics/service.js +21 -0
  134. package/dist/metrics/service.js.map +1 -0
  135. package/dist/presentation/dto/index.d.ts +2 -0
  136. package/dist/presentation/dto/index.d.ts.map +1 -0
  137. package/dist/presentation/dto/index.js +3 -0
  138. package/dist/presentation/dto/index.js.map +1 -0
  139. package/dist/presentation/dto/management.d.ts +7 -0
  140. package/dist/presentation/dto/management.d.ts.map +1 -0
  141. package/dist/presentation/dto/management.js +32 -0
  142. package/dist/presentation/dto/management.js.map +1 -0
  143. package/dist/presentation/e2eRoutes/create-secret.d.ts +2 -0
  144. package/dist/presentation/e2eRoutes/create-secret.d.ts.map +1 -0
  145. package/dist/presentation/e2eRoutes/create-secret.js +34 -0
  146. package/dist/presentation/e2eRoutes/create-secret.js.map +1 -0
  147. package/dist/presentation/e2eRoutes/create-variable.d.ts +2 -0
  148. package/dist/presentation/e2eRoutes/create-variable.d.ts.map +1 -0
  149. package/dist/presentation/e2eRoutes/create-variable.js +34 -0
  150. package/dist/presentation/e2eRoutes/create-variable.js.map +1 -0
  151. package/dist/presentation/e2eRoutes/index.d.ts +3 -0
  152. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
  153. package/dist/presentation/e2eRoutes/index.js +11 -0
  154. package/dist/presentation/e2eRoutes/index.js.map +1 -0
  155. package/dist/presentation/routes/auth.d.ts +16 -0
  156. package/dist/presentation/routes/auth.d.ts.map +1 -0
  157. package/dist/presentation/routes/auth.js +42 -0
  158. package/dist/presentation/routes/auth.js.map +1 -0
  159. package/dist/presentation/routes/batch-secrets.d.ts +2 -0
  160. package/dist/presentation/routes/batch-secrets.d.ts.map +1 -0
  161. package/dist/presentation/routes/batch-secrets.js +44 -0
  162. package/dist/presentation/routes/batch-secrets.js.map +1 -0
  163. package/dist/presentation/routes/batch-variables.d.ts +2 -0
  164. package/dist/presentation/routes/batch-variables.d.ts.map +1 -0
  165. package/dist/presentation/routes/batch-variables.js +44 -0
  166. package/dist/presentation/routes/batch-variables.js.map +1 -0
  167. package/dist/presentation/routes/cursor.d.ts +3 -0
  168. package/dist/presentation/routes/cursor.d.ts.map +1 -0
  169. package/dist/presentation/routes/cursor.js +19 -0
  170. package/dist/presentation/routes/cursor.js.map +1 -0
  171. package/dist/presentation/routes/delete-secret.d.ts +2 -0
  172. package/dist/presentation/routes/delete-secret.d.ts.map +1 -0
  173. package/dist/presentation/routes/delete-secret.js +40 -0
  174. package/dist/presentation/routes/delete-secret.js.map +1 -0
  175. package/dist/presentation/routes/delete-variable.d.ts +2 -0
  176. package/dist/presentation/routes/delete-variable.d.ts.map +1 -0
  177. package/dist/presentation/routes/delete-variable.js +40 -0
  178. package/dist/presentation/routes/delete-variable.js.map +1 -0
  179. package/dist/presentation/routes/errors.d.ts +2 -0
  180. package/dist/presentation/routes/errors.d.ts.map +1 -0
  181. package/dist/presentation/routes/errors.js +73 -0
  182. package/dist/presentation/routes/errors.js.map +1 -0
  183. package/dist/presentation/routes/get-variable.d.ts +2 -0
  184. package/dist/presentation/routes/get-variable.d.ts.map +1 -0
  185. package/dist/presentation/routes/get-variable.js +42 -0
  186. package/dist/presentation/routes/get-variable.js.map +1 -0
  187. package/dist/presentation/routes/index.d.ts +3 -0
  188. package/dist/presentation/routes/index.d.ts.map +1 -0
  189. package/dist/presentation/routes/index.js +29 -0
  190. package/dist/presentation/routes/index.js.map +1 -0
  191. package/dist/presentation/routes/list-secrets.d.ts +2 -0
  192. package/dist/presentation/routes/list-secrets.d.ts.map +1 -0
  193. package/dist/presentation/routes/list-secrets.js +50 -0
  194. package/dist/presentation/routes/list-secrets.js.map +1 -0
  195. package/dist/presentation/routes/list-variables.d.ts +2 -0
  196. package/dist/presentation/routes/list-variables.d.ts.map +1 -0
  197. package/dist/presentation/routes/list-variables.js +50 -0
  198. package/dist/presentation/routes/list-variables.js.map +1 -0
  199. package/dist/presentation/routes/put-secret.d.ts +2 -0
  200. package/dist/presentation/routes/put-secret.d.ts.map +1 -0
  201. package/dist/presentation/routes/put-secret.js +52 -0
  202. package/dist/presentation/routes/put-secret.js.map +1 -0
  203. package/dist/presentation/routes/put-variable.d.ts +2 -0
  204. package/dist/presentation/routes/put-variable.d.ts.map +1 -0
  205. package/dist/presentation/routes/put-variable.js +52 -0
  206. package/dist/presentation/routes/put-variable.js.map +1 -0
  207. package/dist/presentation/routes/warnings.d.ts +5 -0
  208. package/dist/presentation/routes/warnings.d.ts.map +1 -0
  209. package/dist/presentation/routes/warnings.js +16 -0
  210. package/dist/presentation/routes/warnings.js.map +1 -0
  211. package/dist/tsconfig.test.tsbuildinfo +1 -0
  212. package/drizzle/0000_lowly_ogun.sql +59 -0
  213. package/drizzle/meta/0000_snapshot.json +547 -0
  214. package/drizzle/meta/_journal.json +13 -0
  215. package/drizzle.config.ts +7 -0
  216. package/package.json +77 -0
  217. package/src/config.test.ts +41 -0
  218. package/src/config.ts +41 -0
  219. package/src/core/crypto.test.ts +115 -0
  220. package/src/core/crypto.ts +105 -0
  221. package/src/core/dek-manager.test.ts +120 -0
  222. package/src/core/dek-manager.ts +98 -0
  223. package/src/core/errors.ts +100 -0
  224. package/src/core/fingerprint.test.ts +27 -0
  225. package/src/core/fingerprint.ts +14 -0
  226. package/src/core/index.ts +153 -0
  227. package/src/core/key-provider.test.ts +44 -0
  228. package/src/core/key-provider.ts +70 -0
  229. package/src/core/local-secret-store.ts +49 -0
  230. package/src/core/management.test.ts +37 -0
  231. package/src/core/management.ts +503 -0
  232. package/src/core/rotate-kek.test.ts +183 -0
  233. package/src/core/rotate-kek.ts +100 -0
  234. package/src/core/secret-store.test.ts +301 -0
  235. package/src/core/secret-store.ts +251 -0
  236. package/src/core/store-resolver.test.ts +20 -0
  237. package/src/core/store-resolver.ts +28 -0
  238. package/src/core/store-validation.ts +43 -0
  239. package/src/core/value-cipher.ts +26 -0
  240. package/src/core/variable-store.test.ts +106 -0
  241. package/src/core/variable-store.ts +232 -0
  242. package/src/db/cap.ts +58 -0
  243. package/src/db/data-keys.ts +99 -0
  244. package/src/db/db.ts +26 -0
  245. package/src/db/index.ts +45 -0
  246. package/src/db/management.ts +252 -0
  247. package/src/db/schema/common.ts +8 -0
  248. package/src/db/schema/data-keys.ts +31 -0
  249. package/src/db/schema/outbox.ts +4 -0
  250. package/src/db/schema/values.ts +74 -0
  251. package/src/db/schema/variables.ts +71 -0
  252. package/src/db/scope.ts +42 -0
  253. package/src/db/values.ts +191 -0
  254. package/src/db/variables.ts +187 -0
  255. package/src/index.test.ts +11 -0
  256. package/src/index.ts +50 -0
  257. package/src/metrics/index.ts +19 -0
  258. package/src/metrics/instance.ts +219 -0
  259. package/src/metrics/service.ts +19 -0
  260. package/src/presentation/dto/index.ts +1 -0
  261. package/src/presentation/dto/management.ts +36 -0
  262. package/src/presentation/e2eRoutes/create-secret.ts +31 -0
  263. package/src/presentation/e2eRoutes/create-variable.ts +34 -0
  264. package/src/presentation/e2eRoutes/index.test.ts +94 -0
  265. package/src/presentation/e2eRoutes/index.ts +8 -0
  266. package/src/presentation/routes/auth.ts +50 -0
  267. package/src/presentation/routes/batch-secrets.ts +33 -0
  268. package/src/presentation/routes/batch-variables.ts +33 -0
  269. package/src/presentation/routes/cursor.ts +23 -0
  270. package/src/presentation/routes/delete-secret.ts +26 -0
  271. package/src/presentation/routes/delete-variable.ts +26 -0
  272. package/src/presentation/routes/errors.ts +63 -0
  273. package/src/presentation/routes/get-variable.ts +32 -0
  274. package/src/presentation/routes/index.ts +29 -0
  275. package/src/presentation/routes/list-secrets.ts +41 -0
  276. package/src/presentation/routes/list-variables.ts +41 -0
  277. package/src/presentation/routes/management.test.ts +452 -0
  278. package/src/presentation/routes/put-secret.ts +39 -0
  279. package/src/presentation/routes/put-variable.ts +39 -0
  280. package/src/presentation/routes/warnings.ts +23 -0
  281. package/test/env.ts +8 -0
  282. package/test/globalSetup.ts +18 -0
  283. package/test/setup.ts +17 -0
  284. package/tsconfig.build.json +10 -0
  285. package/tsconfig.build.tsbuildinfo +1 -0
  286. package/tsconfig.json +7 -0
  287. package/tsconfig.test.json +8 -0
  288. package/vitest.config.ts +12 -0
@@ -0,0 +1,252 @@
1
+ import {and, asc, eq, gt, inArray, isNull, type SQL, sql} from 'drizzle-orm';
2
+ import type {AnyPgColumn} from 'drizzle-orm/pg-core';
3
+ import type {Tx} from './db.js';
4
+ import {db} from './db.js';
5
+ import {type SecretValue, secretValues} from './schema/values.js';
6
+ import {type SecretVariable, secretVariables, toSecretVariable} from './schema/variables.js';
7
+ import {normalizedProjectId, type StoreScope} from './scope.js';
8
+
9
+ export interface ManagementListParams extends StoreScope {
10
+ workspaceId: string;
11
+ limit: number;
12
+ cursor?: string | undefined;
13
+ }
14
+
15
+ export type SecretManagementRow = Omit<SecretValue, 'ciphertext' | 'fingerprint'>;
16
+
17
+ // Values can be large and multi-line, so the list returns only a bounded, single-line
18
+ // preview (the value's first line, capped at this many characters). The full value is
19
+ // fetched on demand via getVariableManagementRow. This keeps a single-call list of the
20
+ // whole bounded set from materializing hundreds of MB server-side.
21
+ export const VARIABLE_LIST_VALUE_PREVIEW_LENGTH = 256;
22
+
23
+ /** A variable list row whose `value` is a preview; `valueTruncated` flags that more exists. */
24
+ export interface VariableManagementListRow extends SecretVariable {
25
+ valueTruncated: boolean;
26
+ }
27
+
28
+ export interface SecretManagementListResult {
29
+ secrets: SecretManagementRow[];
30
+ nextCursor: string | null;
31
+ }
32
+
33
+ export interface VariableManagementListResult {
34
+ variables: VariableManagementListRow[];
35
+ nextCursor: string | null;
36
+ }
37
+
38
+ export async function listSecretManagementRows(
39
+ params: ManagementListParams,
40
+ tx?: Tx,
41
+ ): Promise<SecretManagementListResult> {
42
+ const executor = tx ?? db();
43
+ const rows = await executor
44
+ .select(secretManagementColumns)
45
+ .from(secretValues)
46
+ .where(and(...managementFilters(secretValues, params)))
47
+ .orderBy(asc(secretValues.key))
48
+ .limit(params.limit + 1);
49
+
50
+ const hasMore = rows.length > params.limit;
51
+ const pageRows = hasMore ? rows.slice(0, params.limit) : rows;
52
+ const last = pageRows.at(-1);
53
+
54
+ return {
55
+ secrets: pageRows.map(toSecretManagementRow),
56
+ nextCursor: hasMore && last ? last.key : null,
57
+ };
58
+ }
59
+
60
+ export async function listVariableManagementRows(
61
+ params: ManagementListParams,
62
+ tx?: Tx,
63
+ ): Promise<VariableManagementListResult> {
64
+ const executor = tx ?? db();
65
+ // Preview = the value's first line, capped at the preview length. `valueTruncated`
66
+ // is true whenever the stored value differs from that preview (longer, or multi-line).
67
+ const valuePreview = sql<string>`left(split_part(${secretVariables.value}, chr(10), 1), ${VARIABLE_LIST_VALUE_PREVIEW_LENGTH})`;
68
+ const rows = await executor
69
+ .select({
70
+ id: secretVariables.id,
71
+ workspaceId: secretVariables.workspaceId,
72
+ projectId: secretVariables.projectId,
73
+ namespace: secretVariables.namespace,
74
+ key: secretVariables.key,
75
+ createdAt: secretVariables.createdAt,
76
+ updatedAt: secretVariables.updatedAt,
77
+ lastEditedBy: secretVariables.lastEditedBy,
78
+ valuePreview,
79
+ valueTruncated: sql<boolean>`${secretVariables.value} <> ${valuePreview}`,
80
+ })
81
+ .from(secretVariables)
82
+ .where(and(...managementFilters(secretVariables, params)))
83
+ .orderBy(asc(secretVariables.key))
84
+ .limit(params.limit + 1);
85
+
86
+ const hasMore = rows.length > params.limit;
87
+ const pageRows = hasMore ? rows.slice(0, params.limit) : rows;
88
+ const last = pageRows.at(-1);
89
+
90
+ return {
91
+ variables: pageRows.map((row) => ({
92
+ id: row.id,
93
+ workspaceId: row.workspaceId,
94
+ projectId: row.projectId,
95
+ namespace: row.namespace,
96
+ key: row.key,
97
+ value: row.valuePreview,
98
+ valueTruncated: row.valueTruncated,
99
+ createdAt: row.createdAt,
100
+ updatedAt: row.updatedAt,
101
+ lastEditedBy: row.lastEditedBy,
102
+ })),
103
+ nextCursor: hasMore && last ? last.key : null,
104
+ };
105
+ }
106
+
107
+ export async function getSecretManagementRow(
108
+ params: StoreScope & {workspaceId: string; key: string},
109
+ tx?: Tx,
110
+ ): Promise<SecretManagementRow | undefined> {
111
+ const executor = tx ?? db();
112
+ const rows = await executor
113
+ .select(secretManagementColumns)
114
+ .from(secretValues)
115
+ .where(and(...managementFilters(secretValues, params), eq(secretValues.key, params.key)))
116
+ .limit(1);
117
+
118
+ const row = rows[0];
119
+ return row ? toSecretManagementRow(row) : undefined;
120
+ }
121
+
122
+ export async function getVariableManagementRow(
123
+ params: StoreScope & {workspaceId: string; key: string},
124
+ tx?: Tx,
125
+ ): Promise<SecretVariable | undefined> {
126
+ const executor = tx ?? db();
127
+ const rows = await executor
128
+ .select()
129
+ .from(secretVariables)
130
+ .where(and(...managementFilters(secretVariables, params), eq(secretVariables.key, params.key)))
131
+ .limit(1);
132
+
133
+ const row = rows[0];
134
+ return row ? toSecretVariable(row) : undefined;
135
+ }
136
+
137
+ export async function listExistingSecretManagementKeys(
138
+ params: StoreScope & {workspaceId: string; keys: string[]},
139
+ tx: Tx,
140
+ ): Promise<Set<string>> {
141
+ if (params.keys.length === 0) return new Set();
142
+
143
+ const rows = await tx
144
+ .select({key: secretValues.key})
145
+ .from(secretValues)
146
+ .where(and(...managementFilters(secretValues, params), inArray(secretValues.key, params.keys)));
147
+
148
+ return new Set(rows.map((row) => row.key));
149
+ }
150
+
151
+ export async function listExistingVariableManagementKeys(
152
+ params: StoreScope & {workspaceId: string; keys: string[]},
153
+ tx: Tx,
154
+ ): Promise<Set<string>> {
155
+ if (params.keys.length === 0) return new Set();
156
+
157
+ const rows = await tx
158
+ .select({key: secretVariables.key})
159
+ .from(secretVariables)
160
+ .where(
161
+ and(...managementFilters(secretVariables, params), inArray(secretVariables.key, params.keys)),
162
+ );
163
+
164
+ return new Set(rows.map((row) => row.key));
165
+ }
166
+
167
+ export async function deleteSecretManagementRows(
168
+ params: StoreScope & {workspaceId: string; keys: string[]},
169
+ tx: Tx,
170
+ ): Promise<SecretManagementRow[]> {
171
+ if (params.keys.length === 0) return [];
172
+
173
+ const rows = await tx
174
+ .delete(secretValues)
175
+ .where(and(...managementFilters(secretValues, params), inArray(secretValues.key, params.keys)))
176
+ .returning(secretManagementColumns);
177
+
178
+ return rows.map(toSecretManagementRow);
179
+ }
180
+
181
+ const secretManagementColumns = {
182
+ id: secretValues.id,
183
+ workspaceId: secretValues.workspaceId,
184
+ projectId: secretValues.projectId,
185
+ namespace: secretValues.namespace,
186
+ key: secretValues.key,
187
+ createdAt: secretValues.createdAt,
188
+ updatedAt: secretValues.updatedAt,
189
+ lastEditedBy: secretValues.lastEditedBy,
190
+ };
191
+
192
+ type SecretManagementDbRow =
193
+ typeof secretManagementColumns extends Record<string, infer _Column>
194
+ ? {
195
+ id: string;
196
+ workspaceId: string;
197
+ projectId: string | null;
198
+ namespace: string;
199
+ key: string;
200
+ createdAt: Date;
201
+ updatedAt: Date;
202
+ lastEditedBy: string | null;
203
+ }
204
+ : never;
205
+
206
+ function toSecretManagementRow(row: SecretManagementDbRow): SecretManagementRow {
207
+ return {
208
+ id: row.id,
209
+ workspaceId: row.workspaceId,
210
+ projectId: row.projectId,
211
+ namespace: row.namespace,
212
+ key: row.key,
213
+ createdAt: row.createdAt,
214
+ updatedAt: row.updatedAt,
215
+ lastEditedBy: row.lastEditedBy,
216
+ };
217
+ }
218
+
219
+ export async function deleteVariableManagementRows(
220
+ params: StoreScope & {workspaceId: string; keys: string[]},
221
+ tx: Tx,
222
+ ): Promise<SecretVariable[]> {
223
+ if (params.keys.length === 0) return [];
224
+
225
+ const rows = await tx
226
+ .delete(secretVariables)
227
+ .where(
228
+ and(...managementFilters(secretVariables, params), inArray(secretVariables.key, params.keys)),
229
+ )
230
+ .returning();
231
+
232
+ return rows.map(toSecretVariable);
233
+ }
234
+
235
+ function managementFilters(
236
+ table: {
237
+ workspaceId: AnyPgColumn;
238
+ projectId: AnyPgColumn;
239
+ namespace: AnyPgColumn;
240
+ key: AnyPgColumn;
241
+ },
242
+ params: StoreScope & {workspaceId: string; cursor?: string | undefined},
243
+ ): SQL[] {
244
+ const projectId = normalizedProjectId(params);
245
+ const filters = [
246
+ eq(table.workspaceId, params.workspaceId),
247
+ eq(table.namespace, ''),
248
+ projectId ? eq(table.projectId, projectId) : isNull(table.projectId),
249
+ ];
250
+ if (params.cursor) filters.push(gt(table.key, params.cursor));
251
+ return filters;
252
+ }
@@ -0,0 +1,8 @@
1
+ import {sql} from 'drizzle-orm';
2
+ import {pgTableCreator} from 'drizzle-orm/pg-core';
3
+
4
+ export const pgTable = pgTableCreator((name) => `secrets_${name}`);
5
+
6
+ export function sqlStringLiteral(value: string) {
7
+ return sql.raw(`'${value.replaceAll("'", "''")}'`);
8
+ }
@@ -0,0 +1,31 @@
1
+ import {text, timestamp, uuid} from 'drizzle-orm/pg-core';
2
+ import {pgTable} from './common.js';
3
+
4
+ export interface DataKey {
5
+ workspaceId: string;
6
+ wrappedDek: string;
7
+ kekVersion: string;
8
+ createdAt: Date;
9
+ rotatedAt: Date | null;
10
+ }
11
+
12
+ export const secretDataKeys = pgTable('data_keys', {
13
+ workspaceId: uuid('workspace_id').primaryKey(),
14
+ wrappedDek: text('wrapped_dek').notNull(),
15
+ kekVersion: text('kek_version').notNull(),
16
+ createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
17
+ rotatedAt: timestamp('rotated_at', {withTimezone: true}),
18
+ });
19
+
20
+ export type DataKeyDb = typeof secretDataKeys.$inferSelect;
21
+ export type DataKeyCreateDb = typeof secretDataKeys.$inferInsert;
22
+
23
+ export function toDataKey(row: DataKeyDb): DataKey {
24
+ return {
25
+ workspaceId: row.workspaceId,
26
+ wrappedDek: row.wrappedDek,
27
+ kekVersion: row.kekVersion,
28
+ createdAt: row.createdAt,
29
+ rotatedAt: row.rotatedAt,
30
+ };
31
+ }
@@ -0,0 +1,4 @@
1
+ import {createOutboxTable} from '@shipfox/node-outbox';
2
+ import {pgTable} from './common.js';
3
+
4
+ export const secretsOutbox = createOutboxTable(pgTable);
@@ -0,0 +1,74 @@
1
+ import {NAMESPACE_PATTERN_SOURCE, SECRET_KEY_PATTERN_SOURCE} from '@shipfox/api-secrets-dto';
2
+ import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
3
+ import {sql} from 'drizzle-orm';
4
+ import {check, index, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
5
+ import {pgTable, sqlStringLiteral} from './common.js';
6
+
7
+ export interface SecretValue {
8
+ id: string;
9
+ workspaceId: string;
10
+ projectId: string | null;
11
+ namespace: string;
12
+ key: string;
13
+ ciphertext: string;
14
+ fingerprint: string | null;
15
+ createdAt: Date;
16
+ updatedAt: Date;
17
+ lastEditedBy: string | null;
18
+ }
19
+
20
+ export const secretValues = pgTable(
21
+ 'values',
22
+ {
23
+ id: uuidv7PrimaryKey(),
24
+ workspaceId: uuid('workspace_id').notNull(),
25
+ projectId: uuid('project_id'),
26
+ namespace: text('namespace').notNull(),
27
+ key: text('key').notNull(),
28
+ ciphertext: text('ciphertext').notNull(),
29
+ fingerprint: text('fingerprint'),
30
+ createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
31
+ updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
32
+ lastEditedBy: uuid('last_edited_by'),
33
+ },
34
+ (table) => [
35
+ uniqueIndex('secrets_values_ws_scope_unique')
36
+ .on(table.workspaceId, table.namespace, table.key)
37
+ .where(sql`"project_id" IS NULL`),
38
+ uniqueIndex('secrets_values_project_scope_unique')
39
+ .on(table.workspaceId, table.projectId, table.namespace, table.key)
40
+ .where(sql`"project_id" IS NOT NULL`),
41
+ index('secrets_values_lookup_idx').on(
42
+ table.workspaceId,
43
+ table.namespace,
44
+ table.projectId,
45
+ table.key,
46
+ ),
47
+ check(
48
+ 'secrets_values_key_ck',
49
+ sql`${table.key} ~ ${sqlStringLiteral(SECRET_KEY_PATTERN_SOURCE)}`,
50
+ ),
51
+ check(
52
+ 'secrets_values_namespace_ck',
53
+ sql`char_length(${table.namespace}) <= 128 AND (${table.namespace} = '' OR ${table.namespace} ~ ${sqlStringLiteral(NAMESPACE_PATTERN_SOURCE)})`,
54
+ ),
55
+ ],
56
+ );
57
+
58
+ export type SecretValueDb = typeof secretValues.$inferSelect;
59
+ export type SecretValueCreateDb = typeof secretValues.$inferInsert;
60
+
61
+ export function toSecretValue(row: SecretValueDb): SecretValue {
62
+ return {
63
+ id: row.id,
64
+ workspaceId: row.workspaceId,
65
+ projectId: row.projectId,
66
+ namespace: row.namespace,
67
+ key: row.key,
68
+ ciphertext: row.ciphertext,
69
+ fingerprint: row.fingerprint,
70
+ createdAt: row.createdAt,
71
+ updatedAt: row.updatedAt,
72
+ lastEditedBy: row.lastEditedBy,
73
+ };
74
+ }
@@ -0,0 +1,71 @@
1
+ import {NAMESPACE_PATTERN_SOURCE, SECRET_KEY_PATTERN_SOURCE} from '@shipfox/api-secrets-dto';
2
+ import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
3
+ import {sql} from 'drizzle-orm';
4
+ import {check, index, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
5
+ import {pgTable, sqlStringLiteral} from './common.js';
6
+
7
+ export interface SecretVariable {
8
+ id: string;
9
+ workspaceId: string;
10
+ projectId: string | null;
11
+ namespace: string;
12
+ key: string;
13
+ value: string;
14
+ createdAt: Date;
15
+ updatedAt: Date;
16
+ lastEditedBy: string | null;
17
+ }
18
+
19
+ export const secretVariables = pgTable(
20
+ 'variables',
21
+ {
22
+ id: uuidv7PrimaryKey(),
23
+ workspaceId: uuid('workspace_id').notNull(),
24
+ projectId: uuid('project_id'),
25
+ namespace: text('namespace').notNull(),
26
+ key: text('key').notNull(),
27
+ value: text('value').notNull(),
28
+ createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
29
+ updatedAt: timestamp('updated_at', {withTimezone: true}).notNull().defaultNow(),
30
+ lastEditedBy: uuid('last_edited_by'),
31
+ },
32
+ (table) => [
33
+ uniqueIndex('secrets_variables_ws_scope_unique')
34
+ .on(table.workspaceId, table.namespace, table.key)
35
+ .where(sql`"project_id" IS NULL`),
36
+ uniqueIndex('secrets_variables_project_scope_unique')
37
+ .on(table.workspaceId, table.projectId, table.namespace, table.key)
38
+ .where(sql`"project_id" IS NOT NULL`),
39
+ index('secrets_variables_lookup_idx').on(
40
+ table.workspaceId,
41
+ table.namespace,
42
+ table.projectId,
43
+ table.key,
44
+ ),
45
+ check(
46
+ 'secrets_variables_key_ck',
47
+ sql`${table.key} ~ ${sqlStringLiteral(SECRET_KEY_PATTERN_SOURCE)}`,
48
+ ),
49
+ check(
50
+ 'secrets_variables_namespace_ck',
51
+ sql`char_length(${table.namespace}) <= 128 AND (${table.namespace} = '' OR ${table.namespace} ~ ${sqlStringLiteral(NAMESPACE_PATTERN_SOURCE)})`,
52
+ ),
53
+ ],
54
+ );
55
+
56
+ export type SecretVariableDb = typeof secretVariables.$inferSelect;
57
+ export type SecretVariableCreateDb = typeof secretVariables.$inferInsert;
58
+
59
+ export function toSecretVariable(row: SecretVariableDb): SecretVariable {
60
+ return {
61
+ id: row.id,
62
+ workspaceId: row.workspaceId,
63
+ projectId: row.projectId,
64
+ namespace: row.namespace,
65
+ key: row.key,
66
+ value: row.value,
67
+ createdAt: row.createdAt,
68
+ updatedAt: row.updatedAt,
69
+ lastEditedBy: row.lastEditedBy,
70
+ };
71
+ }
@@ -0,0 +1,42 @@
1
+ import {and, eq, isNull, or, type SQL, sql} from 'drizzle-orm';
2
+ import type {PgColumn} from 'drizzle-orm/pg-core';
3
+
4
+ export interface StoreScope {
5
+ projectId?: string | null | undefined;
6
+ }
7
+
8
+ export interface ScopeColumns {
9
+ workspaceId: PgColumn;
10
+ projectId: PgColumn;
11
+ namespace: PgColumn;
12
+ key: PgColumn;
13
+ }
14
+
15
+ export function normalizedProjectId(scope?: StoreScope | undefined): string | null {
16
+ return scope?.projectId || null;
17
+ }
18
+
19
+ export function scopeConflictTargetWhere(scope?: StoreScope | undefined): SQL {
20
+ return normalizedProjectId(scope) !== null
21
+ ? sql`"project_id" IS NOT NULL`
22
+ : sql`"project_id" IS NULL`;
23
+ }
24
+
25
+ export function scopeExactWhere(columns: ScopeColumns, params: StoreScope): SQL | undefined {
26
+ const projectId = normalizedProjectId(params);
27
+ return projectId !== null ? eq(columns.projectId, projectId) : isNull(columns.projectId);
28
+ }
29
+
30
+ export function lookupWithPrecedenceWhere(
31
+ columns: ScopeColumns,
32
+ params: StoreScope & {workspaceId: string; namespace: string; key: string},
33
+ ): SQL | undefined {
34
+ const base = and(
35
+ eq(columns.workspaceId, params.workspaceId),
36
+ eq(columns.namespace, params.namespace),
37
+ eq(columns.key, params.key),
38
+ );
39
+ const projectId = normalizedProjectId(params);
40
+ if (projectId === null) return and(base, isNull(columns.projectId));
41
+ return and(base, or(eq(columns.projectId, projectId), isNull(columns.projectId)));
42
+ }
@@ -0,0 +1,191 @@
1
+ import {and, eq, inArray, isNull, sql} from 'drizzle-orm';
2
+ import {SecretBatchScopeMismatchError} from '#core/errors.js';
3
+ import {db, type Tx} from './db.js';
4
+ import {type SecretValue, secretValues, toSecretValue} from './schema/values.js';
5
+ import {
6
+ lookupWithPrecedenceWhere,
7
+ normalizedProjectId,
8
+ type StoreScope,
9
+ scopeConflictTargetWhere,
10
+ scopeExactWhere,
11
+ } from './scope.js';
12
+
13
+ export interface SecretValueWrite {
14
+ workspaceId: string;
15
+ projectId: string | null;
16
+ namespace: string;
17
+ key: string;
18
+ ciphertext: string;
19
+ fingerprint: string | null;
20
+ lastEditedBy?: string | null | undefined;
21
+ }
22
+
23
+ export async function getSecretValueRowWithPrecedence(
24
+ params: StoreScope & {workspaceId: string; namespace: string; key: string},
25
+ tx?: Tx,
26
+ ): Promise<SecretValue | undefined> {
27
+ const executor = tx ?? db();
28
+ const rows = await executor
29
+ .select()
30
+ .from(secretValues)
31
+ .where(
32
+ lookupWithPrecedenceWhere(
33
+ {
34
+ workspaceId: secretValues.workspaceId,
35
+ projectId: secretValues.projectId,
36
+ namespace: secretValues.namespace,
37
+ key: secretValues.key,
38
+ },
39
+ params,
40
+ ),
41
+ )
42
+ .orderBy(sql`${secretValues.projectId} NULLS LAST`)
43
+ .limit(1);
44
+ const row = rows[0];
45
+ return row ? toSecretValue(row) : undefined;
46
+ }
47
+
48
+ export async function listSecretValueRowsByNamespace(
49
+ params: StoreScope & {workspaceId: string; namespace: string},
50
+ tx?: Tx,
51
+ ): Promise<SecretValue[]> {
52
+ const executor = tx ?? db();
53
+ const rows = await executor.execute<SecretValueRow>(sql`
54
+ SELECT DISTINCT ON (key)
55
+ id, workspace_id, project_id, namespace, key, ciphertext, fingerprint,
56
+ created_at, updated_at, last_edited_by
57
+ FROM secrets_values
58
+ WHERE ${listByNamespaceSql(params)}
59
+ ORDER BY key, project_id NULLS LAST, id
60
+ `);
61
+ return rows.rows.map(rowToSecretValue);
62
+ }
63
+
64
+ export async function upsertSecretValueRows(rows: SecretValueWrite[], tx: Tx): Promise<void> {
65
+ if (rows.length === 0) return;
66
+ const projectId = batchProjectId(rows);
67
+ await tx
68
+ .insert(secretValues)
69
+ .values(rows)
70
+ .onConflictDoUpdate({
71
+ target: projectId
72
+ ? [
73
+ secretValues.workspaceId,
74
+ secretValues.projectId,
75
+ secretValues.namespace,
76
+ secretValues.key,
77
+ ]
78
+ : [secretValues.workspaceId, secretValues.namespace, secretValues.key],
79
+ targetWhere: scopeConflictTargetWhere({projectId}),
80
+ set: {
81
+ ciphertext: sql`excluded.ciphertext`,
82
+ fingerprint: sql`excluded.fingerprint`,
83
+ lastEditedBy: sql`excluded.last_edited_by`,
84
+ updatedAt: sql`NOW()`,
85
+ },
86
+ });
87
+ }
88
+
89
+ export async function countSecretValueRows(
90
+ params: StoreScope & {workspaceId: string; namespace: string; keys: string[]},
91
+ tx: Tx,
92
+ ): Promise<number> {
93
+ if (params.keys.length === 0) return 0;
94
+
95
+ const rows = await tx
96
+ .select({count: sql<string | number>`COUNT(*)`})
97
+ .from(secretValues)
98
+ .where(
99
+ and(
100
+ eq(secretValues.workspaceId, params.workspaceId),
101
+ eq(secretValues.namespace, params.namespace),
102
+ scopeExactWhere(
103
+ {
104
+ workspaceId: secretValues.workspaceId,
105
+ projectId: secretValues.projectId,
106
+ namespace: secretValues.namespace,
107
+ key: secretValues.key,
108
+ },
109
+ params,
110
+ ) ?? isNull(secretValues.projectId),
111
+ inArray(secretValues.key, params.keys),
112
+ ),
113
+ );
114
+
115
+ return Number(rows[0]?.count ?? 0);
116
+ }
117
+
118
+ export async function deleteSecretValueRows(
119
+ params: StoreScope & {workspaceId: string; namespace: string; keys?: string[] | undefined},
120
+ tx?: Tx,
121
+ ): Promise<number> {
122
+ if (params.keys && params.keys.length === 0) return 0;
123
+
124
+ const executor = tx ?? db();
125
+ const filters = [
126
+ eq(secretValues.workspaceId, params.workspaceId),
127
+ eq(secretValues.namespace, params.namespace),
128
+ scopeExactWhere(
129
+ {
130
+ workspaceId: secretValues.workspaceId,
131
+ projectId: secretValues.projectId,
132
+ namespace: secretValues.namespace,
133
+ key: secretValues.key,
134
+ },
135
+ params,
136
+ ) ?? isNull(secretValues.projectId),
137
+ ];
138
+ if (params.keys && params.keys.length > 0) {
139
+ filters.push(inArray(secretValues.key, params.keys));
140
+ }
141
+ const deleted = await executor
142
+ .delete(secretValues)
143
+ .where(and(...filters))
144
+ .returning({id: secretValues.id});
145
+ return deleted.length;
146
+ }
147
+
148
+ function batchProjectId(rows: SecretValueWrite[]): string | null {
149
+ const projectId = normalizedProjectId({projectId: rows[0]?.projectId});
150
+ const mixedScope = rows.some(
151
+ (row) => normalizedProjectId({projectId: row.projectId}) !== projectId,
152
+ );
153
+ if (mixedScope) throw new SecretBatchScopeMismatchError();
154
+ return projectId;
155
+ }
156
+
157
+ function listByNamespaceSql(params: StoreScope & {workspaceId: string; namespace: string}) {
158
+ const projectId = normalizedProjectId(params);
159
+ if (!projectId) {
160
+ return sql`workspace_id = ${params.workspaceId} AND namespace = ${params.namespace} AND project_id IS NULL`;
161
+ }
162
+ return sql`workspace_id = ${params.workspaceId} AND namespace = ${params.namespace} AND (project_id = ${projectId} OR project_id IS NULL)`;
163
+ }
164
+
165
+ interface SecretValueRow extends Record<string, unknown> {
166
+ id: string;
167
+ workspace_id: string;
168
+ project_id: string | null;
169
+ namespace: string;
170
+ key: string;
171
+ ciphertext: string;
172
+ fingerprint: string | null;
173
+ created_at: Date;
174
+ updated_at: Date;
175
+ last_edited_by: string | null;
176
+ }
177
+
178
+ function rowToSecretValue(row: SecretValueRow): SecretValue {
179
+ return {
180
+ id: row.id,
181
+ workspaceId: row.workspace_id,
182
+ projectId: row.project_id,
183
+ namespace: row.namespace,
184
+ key: row.key,
185
+ ciphertext: row.ciphertext,
186
+ fingerprint: row.fingerprint,
187
+ createdAt: row.created_at,
188
+ updatedAt: row.updated_at,
189
+ lastEditedBy: row.last_edited_by,
190
+ };
191
+ }