@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,187 @@
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 SecretVariable, secretVariables, toSecretVariable} from './schema/variables.js';
5
+ import {
6
+ lookupWithPrecedenceWhere,
7
+ normalizedProjectId,
8
+ type StoreScope,
9
+ scopeConflictTargetWhere,
10
+ scopeExactWhere,
11
+ } from './scope.js';
12
+
13
+ export interface SecretVariableWrite {
14
+ workspaceId: string;
15
+ projectId: string | null;
16
+ namespace: string;
17
+ key: string;
18
+ value: string;
19
+ lastEditedBy?: string | null | undefined;
20
+ }
21
+
22
+ export async function getSecretVariableRowWithPrecedence(
23
+ params: StoreScope & {workspaceId: string; namespace: string; key: string},
24
+ tx?: Tx,
25
+ ): Promise<SecretVariable | undefined> {
26
+ const executor = tx ?? db();
27
+ const rows = await executor
28
+ .select()
29
+ .from(secretVariables)
30
+ .where(
31
+ lookupWithPrecedenceWhere(
32
+ {
33
+ workspaceId: secretVariables.workspaceId,
34
+ projectId: secretVariables.projectId,
35
+ namespace: secretVariables.namespace,
36
+ key: secretVariables.key,
37
+ },
38
+ params,
39
+ ),
40
+ )
41
+ .orderBy(sql`${secretVariables.projectId} NULLS LAST`)
42
+ .limit(1);
43
+ const row = rows[0];
44
+ return row ? toSecretVariable(row) : undefined;
45
+ }
46
+
47
+ export async function listSecretVariableRowsByNamespace(
48
+ params: StoreScope & {workspaceId: string; namespace: string},
49
+ tx?: Tx,
50
+ ): Promise<SecretVariable[]> {
51
+ const executor = tx ?? db();
52
+ const rows = await executor.execute<SecretVariableRow>(sql`
53
+ SELECT DISTINCT ON (key)
54
+ id, workspace_id, project_id, namespace, key, value,
55
+ created_at, updated_at, last_edited_by
56
+ FROM secrets_variables
57
+ WHERE ${listByNamespaceSql(params)}
58
+ ORDER BY key, project_id NULLS LAST, id
59
+ `);
60
+ return rows.rows.map(rowToSecretVariable);
61
+ }
62
+
63
+ export async function upsertSecretVariableRows(rows: SecretVariableWrite[], tx: Tx): Promise<void> {
64
+ if (rows.length === 0) return;
65
+ const projectId = batchProjectId(rows);
66
+ await tx
67
+ .insert(secretVariables)
68
+ .values(rows)
69
+ .onConflictDoUpdate({
70
+ target: projectId
71
+ ? [
72
+ secretVariables.workspaceId,
73
+ secretVariables.projectId,
74
+ secretVariables.namespace,
75
+ secretVariables.key,
76
+ ]
77
+ : [secretVariables.workspaceId, secretVariables.namespace, secretVariables.key],
78
+ targetWhere: scopeConflictTargetWhere({projectId}),
79
+ set: {
80
+ value: sql`excluded.value`,
81
+ lastEditedBy: sql`excluded.last_edited_by`,
82
+ updatedAt: sql`NOW()`,
83
+ },
84
+ });
85
+ }
86
+
87
+ export async function countSecretVariableRows(
88
+ params: StoreScope & {workspaceId: string; namespace: string; keys: string[]},
89
+ tx: Tx,
90
+ ): Promise<number> {
91
+ if (params.keys.length === 0) return 0;
92
+
93
+ const rows = await tx
94
+ .select({count: sql<string | number>`COUNT(*)`})
95
+ .from(secretVariables)
96
+ .where(
97
+ and(
98
+ eq(secretVariables.workspaceId, params.workspaceId),
99
+ eq(secretVariables.namespace, params.namespace),
100
+ scopeExactWhere(
101
+ {
102
+ workspaceId: secretVariables.workspaceId,
103
+ projectId: secretVariables.projectId,
104
+ namespace: secretVariables.namespace,
105
+ key: secretVariables.key,
106
+ },
107
+ params,
108
+ ) ?? isNull(secretVariables.projectId),
109
+ inArray(secretVariables.key, params.keys),
110
+ ),
111
+ );
112
+
113
+ return Number(rows[0]?.count ?? 0);
114
+ }
115
+
116
+ export async function deleteSecretVariableRows(
117
+ params: StoreScope & {workspaceId: string; namespace: string; keys?: string[] | undefined},
118
+ tx?: Tx,
119
+ ): Promise<number> {
120
+ if (params.keys && params.keys.length === 0) return 0;
121
+
122
+ const executor = tx ?? db();
123
+ const filters = [
124
+ eq(secretVariables.workspaceId, params.workspaceId),
125
+ eq(secretVariables.namespace, params.namespace),
126
+ scopeExactWhere(
127
+ {
128
+ workspaceId: secretVariables.workspaceId,
129
+ projectId: secretVariables.projectId,
130
+ namespace: secretVariables.namespace,
131
+ key: secretVariables.key,
132
+ },
133
+ params,
134
+ ) ?? isNull(secretVariables.projectId),
135
+ ];
136
+ if (params.keys && params.keys.length > 0) {
137
+ filters.push(inArray(secretVariables.key, params.keys));
138
+ }
139
+ const deleted = await executor
140
+ .delete(secretVariables)
141
+ .where(and(...filters))
142
+ .returning({id: secretVariables.id});
143
+ return deleted.length;
144
+ }
145
+
146
+ function batchProjectId(rows: SecretVariableWrite[]): string | null {
147
+ const projectId = normalizedProjectId({projectId: rows[0]?.projectId});
148
+ const mixedScope = rows.some(
149
+ (row) => normalizedProjectId({projectId: row.projectId}) !== projectId,
150
+ );
151
+ if (mixedScope) throw new SecretBatchScopeMismatchError();
152
+ return projectId;
153
+ }
154
+
155
+ function listByNamespaceSql(params: StoreScope & {workspaceId: string; namespace: string}) {
156
+ const projectId = normalizedProjectId(params);
157
+ if (!projectId) {
158
+ return sql`workspace_id = ${params.workspaceId} AND namespace = ${params.namespace} AND project_id IS NULL`;
159
+ }
160
+ return sql`workspace_id = ${params.workspaceId} AND namespace = ${params.namespace} AND (project_id = ${projectId} OR project_id IS NULL)`;
161
+ }
162
+
163
+ interface SecretVariableRow extends Record<string, unknown> {
164
+ id: string;
165
+ workspace_id: string;
166
+ project_id: string | null;
167
+ namespace: string;
168
+ key: string;
169
+ value: string;
170
+ created_at: Date;
171
+ updated_at: Date;
172
+ last_edited_by: string | null;
173
+ }
174
+
175
+ function rowToSecretVariable(row: SecretVariableRow): SecretVariable {
176
+ return {
177
+ id: row.id,
178
+ workspaceId: row.workspace_id,
179
+ projectId: row.project_id,
180
+ namespace: row.namespace,
181
+ key: row.key,
182
+ value: row.value,
183
+ createdAt: row.created_at,
184
+ updatedAt: row.updated_at,
185
+ lastEditedBy: row.last_edited_by,
186
+ };
187
+ }
@@ -0,0 +1,11 @@
1
+ import {describe, expect, it} from '@shipfox/vitest/vi';
2
+ import {SecretValueTooLargeError} from './index.js';
3
+
4
+ describe('package exports', () => {
5
+ it('exports the oversized secret value error from the package root', () => {
6
+ const error = new SecretValueTooLargeError(64 * 1024);
7
+
8
+ expect(error).toBeInstanceOf(Error);
9
+ expect(error.maxBytes).toBe(64 * 1024);
10
+ });
11
+ });
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ import {secretsEventSchemas} from '@shipfox/api-secrets-dto';
2
+ import type {ShipfoxModule} from '@shipfox/node-module';
3
+ import {db, migrationsPath, secretsOutbox} from '#db/index.js';
4
+ import {registerSecretsServiceMetrics} from '#metrics/index.js';
5
+ import {secretsE2eRoutes} from '#presentation/e2eRoutes/index.js';
6
+ import {secretsRoutes} from '#presentation/routes/index.js';
7
+
8
+ export {
9
+ BUILTIN_LOCAL_STORE,
10
+ DekUnwrapError,
11
+ DekWrapError,
12
+ deleteManagedSecret,
13
+ deleteManagedVariable,
14
+ deleteSecrets,
15
+ deleteVariables,
16
+ getManagedVariable,
17
+ getSecret,
18
+ getSecretsByNamespace,
19
+ getVariable,
20
+ getVariablesByNamespace,
21
+ KekConfigurationError,
22
+ KekVersionStrandedError,
23
+ listManagedSecrets,
24
+ listManagedVariables,
25
+ NamespaceValidationError,
26
+ resolveSecretStore,
27
+ rotateWorkspaceDataKeys,
28
+ SecretBatchScopeMismatchError,
29
+ SecretDecryptionError,
30
+ SecretKeyValidationError,
31
+ SecretNotFoundError,
32
+ type SecretStoreProvider,
33
+ SecretValueTooLargeError,
34
+ setManagedSecrets,
35
+ setManagedVariables,
36
+ setSecrets,
37
+ setVariables,
38
+ UnknownSecretStoreError,
39
+ VariableNotFoundError,
40
+ WorkspaceSecretCapExceededError,
41
+ } from '#core/index.js';
42
+
43
+ export const secretsModule: ShipfoxModule = {
44
+ name: 'secrets',
45
+ database: {db, migrationsPath},
46
+ routes: secretsRoutes,
47
+ e2eRoutes: [secretsE2eRoutes],
48
+ metrics: registerSecretsServiceMetrics,
49
+ publishers: [{name: 'secrets', table: secretsOutbox, db, eventSchemas: secretsEventSchemas}],
50
+ };
@@ -0,0 +1,19 @@
1
+ export {
2
+ classifyDekAccessError,
3
+ classifyKekRotationError,
4
+ classifySecretsOperationError,
5
+ operationScope,
6
+ recordSecretsDekAccess,
7
+ recordSecretsEntriesMutated,
8
+ recordSecretsKekRotation,
9
+ recordSecretsOperation,
10
+ type SecretsDekAccessOutcome,
11
+ type SecretsKekRotationOutcome,
12
+ type SecretsMetricOperation,
13
+ type SecretsMetricResource,
14
+ type SecretsMetricScope,
15
+ type SecretsMetricSurface,
16
+ type SecretsMutationEffect,
17
+ type SecretsOperationOutcome,
18
+ } from './instance.js';
19
+ export {registerSecretsServiceMetrics} from './service.js';
@@ -0,0 +1,219 @@
1
+ import {instanceMetrics} from '@shipfox/node-opentelemetry';
2
+ import {
3
+ DekUnwrapError,
4
+ DekWrapError,
5
+ KekVersionStrandedError,
6
+ NamespaceValidationError,
7
+ SecretBatchDuplicateKeyError,
8
+ SecretBatchScopeMismatchError,
9
+ SecretDecryptionError,
10
+ SecretKeyValidationError,
11
+ SecretNotFoundError,
12
+ SecretValueTooLargeError,
13
+ UnknownSecretStoreError,
14
+ VariableNotFoundError,
15
+ WorkspaceSecretCapExceededError,
16
+ } from '#core/errors.js';
17
+
18
+ const meter = instanceMetrics.getMeter('secrets');
19
+
20
+ export type SecretsMetricResource = 'secret' | 'variable';
21
+ export type SecretsMetricOperation = 'get' | 'get_namespace' | 'set' | 'delete' | 'list';
22
+ export type SecretsMetricSurface = 'internal' | 'management';
23
+ export type SecretsMetricScope = 'workspace' | 'project';
24
+ export type SecretsOperationOutcome =
25
+ | 'success'
26
+ | 'not_found'
27
+ | 'validation_failed'
28
+ | 'cap_exceeded'
29
+ | 'value_too_large'
30
+ | 'decryption_failed'
31
+ | 'crypto_failed'
32
+ | 'failure';
33
+ export type SecretsMutationEffect = 'created' | 'updated' | 'deleted';
34
+ export type SecretsDekAccessOutcome =
35
+ | 'cache_hit'
36
+ | 'cache_expired'
37
+ | 'db_unwrapped'
38
+ | 'generated'
39
+ | 'unwrap_failed'
40
+ | 'wrap_failed'
41
+ | 'failure';
42
+ export type SecretsKekRotationOutcome =
43
+ | 'rotated'
44
+ | 'skipped_current'
45
+ | 'skipped_race'
46
+ | 'none'
47
+ | 'stranded'
48
+ | 'failure';
49
+
50
+ const operationCount = meter.createCounter<{
51
+ resource: SecretsMetricResource;
52
+ operation: SecretsMetricOperation;
53
+ surface: SecretsMetricSurface;
54
+ scope: SecretsMetricScope;
55
+ outcome: SecretsOperationOutcome;
56
+ }>('secrets_operation', {
57
+ description: 'Secrets module operation attempts by resource, surface, scope, and outcome',
58
+ });
59
+
60
+ const operationDuration = meter.createHistogram<{
61
+ resource: SecretsMetricResource;
62
+ operation: SecretsMetricOperation;
63
+ surface: SecretsMetricSurface;
64
+ scope: SecretsMetricScope;
65
+ outcome: SecretsOperationOutcome;
66
+ }>('secrets_operation_duration', {
67
+ description: 'Secrets module operation duration',
68
+ unit: 'ms',
69
+ advice: {explicitBucketBoundaries: [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000]},
70
+ });
71
+
72
+ const entriesMutatedCount = meter.createCounter<{
73
+ resource: SecretsMetricResource;
74
+ operation: 'set' | 'delete';
75
+ effect: SecretsMutationEffect;
76
+ surface: SecretsMetricSurface;
77
+ }>('secrets_entries_mutated', {
78
+ description: 'Secret and variable entries mutated by operation, effect, and surface',
79
+ });
80
+
81
+ const dekAccessCount = meter.createCounter<{outcome: SecretsDekAccessOutcome}>(
82
+ 'secrets_dek_access',
83
+ {description: 'Plaintext data-encryption key access attempts by outcome'},
84
+ );
85
+
86
+ const dekAccessDuration = meter.createHistogram<{outcome: SecretsDekAccessOutcome}>(
87
+ 'secrets_dek_access_duration',
88
+ {
89
+ description: 'Plaintext data-encryption key access duration',
90
+ unit: 'ms',
91
+ advice: {explicitBucketBoundaries: [0, 1, 5, 10, 25, 50, 100, 250, 500, 1000]},
92
+ },
93
+ );
94
+
95
+ const kekRotationCount = meter.createCounter<{outcome: SecretsKekRotationOutcome}>(
96
+ 'secrets_kek_rotation',
97
+ {description: 'Workspace data-key KEK rotation outcomes'},
98
+ );
99
+
100
+ const kekRotationDuration = meter.createHistogram<{outcome: SecretsKekRotationOutcome}>(
101
+ 'secrets_kek_rotation_duration',
102
+ {
103
+ description: 'Workspace data-key KEK rotation duration',
104
+ unit: 'ms',
105
+ advice: {explicitBucketBoundaries: [10, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000]},
106
+ },
107
+ );
108
+
109
+ function recordMetric(record: () => void): void {
110
+ try {
111
+ record();
112
+ } catch {
113
+ // Metrics must not affect secret storage outcomes.
114
+ }
115
+ }
116
+
117
+ export function operationScope(params: {
118
+ projectId?: string | null | undefined;
119
+ }): SecretsMetricScope {
120
+ return params.projectId ? 'project' : 'workspace';
121
+ }
122
+
123
+ export function classifySecretsOperationError(error: unknown): SecretsOperationOutcome {
124
+ if (error instanceof SecretNotFoundError || error instanceof VariableNotFoundError) {
125
+ return 'not_found';
126
+ }
127
+ if (error instanceof WorkspaceSecretCapExceededError) return 'cap_exceeded';
128
+ if (error instanceof SecretValueTooLargeError) return 'value_too_large';
129
+ if (error instanceof SecretDecryptionError) return 'decryption_failed';
130
+ if (error instanceof DekUnwrapError || error instanceof DekWrapError) return 'crypto_failed';
131
+ if (
132
+ error instanceof NamespaceValidationError ||
133
+ error instanceof SecretKeyValidationError ||
134
+ error instanceof SecretBatchDuplicateKeyError ||
135
+ error instanceof SecretBatchScopeMismatchError ||
136
+ error instanceof UnknownSecretStoreError
137
+ ) {
138
+ return 'validation_failed';
139
+ }
140
+ return 'failure';
141
+ }
142
+
143
+ export function recordSecretsOperation(params: {
144
+ resource: SecretsMetricResource;
145
+ operation: SecretsMetricOperation;
146
+ surface: SecretsMetricSurface;
147
+ scope: SecretsMetricScope;
148
+ outcome: SecretsOperationOutcome;
149
+ durationMs: number;
150
+ }): void {
151
+ recordMetric(() => {
152
+ const labels = {
153
+ resource: params.resource,
154
+ operation: params.operation,
155
+ surface: params.surface,
156
+ scope: params.scope,
157
+ outcome: params.outcome,
158
+ };
159
+ operationCount.add(1, labels);
160
+ operationDuration.record(params.durationMs, labels);
161
+ });
162
+ }
163
+
164
+ export function recordSecretsEntriesMutated(params: {
165
+ resource: SecretsMetricResource;
166
+ operation: 'set' | 'delete';
167
+ effect: SecretsMutationEffect;
168
+ surface: SecretsMetricSurface;
169
+ count: number;
170
+ }): void {
171
+ if (params.count <= 0) return;
172
+
173
+ recordMetric(() =>
174
+ entriesMutatedCount.add(params.count, {
175
+ resource: params.resource,
176
+ operation: params.operation,
177
+ effect: params.effect,
178
+ surface: params.surface,
179
+ }),
180
+ );
181
+ }
182
+
183
+ export function recordSecretsDekAccess(params: {
184
+ outcome: SecretsDekAccessOutcome;
185
+ durationMs: number;
186
+ }): void {
187
+ recordMetric(() => {
188
+ dekAccessCount.add(1, {outcome: params.outcome});
189
+ dekAccessDuration.record(params.durationMs, {outcome: params.outcome});
190
+ });
191
+ }
192
+
193
+ export function classifyDekAccessError(error: unknown): SecretsDekAccessOutcome {
194
+ if (error instanceof DekUnwrapError) return 'unwrap_failed';
195
+ if (error instanceof DekWrapError) return 'wrap_failed';
196
+ return 'failure';
197
+ }
198
+
199
+ export function recordSecretsKekRotation(params: {
200
+ outcome: SecretsKekRotationOutcome;
201
+ count?: number | undefined;
202
+ durationMs?: number | undefined;
203
+ }): void {
204
+ const count = params.count ?? 1;
205
+ if (count <= 0 && params.durationMs === undefined) return;
206
+
207
+ recordMetric(() => {
208
+ if (count > 0) kekRotationCount.add(count, {outcome: params.outcome});
209
+ if (params.durationMs !== undefined) {
210
+ kekRotationDuration.record(params.durationMs, {outcome: params.outcome});
211
+ }
212
+ });
213
+ }
214
+
215
+ export function classifyKekRotationError(error: unknown): SecretsKekRotationOutcome {
216
+ if (error instanceof KekVersionStrandedError) return 'stranded';
217
+ if (error instanceof DekUnwrapError || error instanceof DekWrapError) return 'failure';
218
+ return 'failure';
219
+ }
@@ -0,0 +1,19 @@
1
+ import {getServiceMetricsProvider} from '@shipfox/node-opentelemetry';
2
+ import {countPublicEntriesByResource} from '#db/index.js';
3
+
4
+ export function registerSecretsServiceMetrics(): void {
5
+ const meter = getServiceMetricsProvider().getMeter('secrets');
6
+
7
+ const publicEntries = meter.createObservableGauge('secrets_public_entries', {
8
+ description: 'Public secret and variable entries currently stored, excluding system namespaces',
9
+ });
10
+
11
+ meter.addBatchObservableCallback(
12
+ async (observer) => {
13
+ const counts = await countPublicEntriesByResource();
14
+ observer.observe(publicEntries, counts.secrets, {resource: 'secret'});
15
+ observer.observe(publicEntries, counts.variables, {resource: 'variable'});
16
+ },
17
+ [publicEntries],
18
+ );
19
+ }
@@ -0,0 +1 @@
1
+ export {toSecretDto, toVariableDto, toVariableListItemDto} from './management.js';
@@ -0,0 +1,36 @@
1
+ import type {SecretDto, VariableDto, VariableListItemDto} from '@shipfox/api-secrets-dto';
2
+ import type {SecretManagementRow, VariableManagementListRow} from '#db/index.js';
3
+ import type {SecretVariable} from '#db/schema/variables.js';
4
+
5
+ export function toSecretDto(secret: SecretManagementRow): SecretDto {
6
+ return {
7
+ key: secret.key,
8
+ project_id: secret.projectId,
9
+ created_at: secret.createdAt.toISOString(),
10
+ updated_at: secret.updatedAt.toISOString(),
11
+ last_edited_by: secret.lastEditedBy,
12
+ };
13
+ }
14
+
15
+ export function toVariableDto(variable: SecretVariable): VariableDto {
16
+ return {
17
+ key: variable.key,
18
+ project_id: variable.projectId,
19
+ value: variable.value,
20
+ created_at: variable.createdAt.toISOString(),
21
+ updated_at: variable.updatedAt.toISOString(),
22
+ last_edited_by: variable.lastEditedBy,
23
+ };
24
+ }
25
+
26
+ export function toVariableListItemDto(variable: VariableManagementListRow): VariableListItemDto {
27
+ return {
28
+ key: variable.key,
29
+ project_id: variable.projectId,
30
+ value: variable.value,
31
+ value_truncated: variable.valueTruncated,
32
+ created_at: variable.createdAt.toISOString(),
33
+ updated_at: variable.updatedAt.toISOString(),
34
+ last_edited_by: variable.lastEditedBy,
35
+ };
36
+ }
@@ -0,0 +1,31 @@
1
+ import {e2eCreateSecretBodySchema, e2eCreateSecretResponseSchema} from '@shipfox/api-secrets-dto';
2
+ import {defineRoute} from '@shipfox/node-fastify';
3
+ import {setManagedSecrets} from '#core/index.js';
4
+ import {toSecretDto} from '#presentation/dto/index.js';
5
+ import {translateManagementError} from '#presentation/routes/errors.js';
6
+
7
+ type ManagedSecret = Awaited<ReturnType<typeof setManagedSecrets>>[number];
8
+
9
+ export const createE2eSecretRoute = defineRoute({
10
+ method: 'POST',
11
+ path: '/secret',
12
+ description: 'Create a workspace secret for E2E tests.',
13
+ schema: {
14
+ body: e2eCreateSecretBodySchema,
15
+ response: {201: e2eCreateSecretResponseSchema},
16
+ },
17
+ errorHandler: translateManagementError,
18
+ handler: async (request, reply) => {
19
+ const secret = (
20
+ await setManagedSecrets({
21
+ workspaceId: request.body.workspace_id,
22
+ actorId: request.body.actor_id,
23
+ projectId: request.body.project_id,
24
+ entries: [{key: request.body.key, value: request.body.value}],
25
+ })
26
+ )[0] as ManagedSecret;
27
+
28
+ reply.code(201);
29
+ return toSecretDto(secret);
30
+ },
31
+ });
@@ -0,0 +1,34 @@
1
+ import {
2
+ e2eCreateVariableBodySchema,
3
+ e2eCreateVariableResponseSchema,
4
+ } from '@shipfox/api-secrets-dto';
5
+ import {defineRoute} from '@shipfox/node-fastify';
6
+ import {setManagedVariables} from '#core/index.js';
7
+ import {toVariableDto} from '#presentation/dto/index.js';
8
+ import {translateManagementError} from '#presentation/routes/errors.js';
9
+
10
+ type ManagedVariable = Awaited<ReturnType<typeof setManagedVariables>>[number];
11
+
12
+ export const createE2eVariableRoute = defineRoute({
13
+ method: 'POST',
14
+ path: '/variable',
15
+ description: 'Create a workspace variable for E2E tests.',
16
+ schema: {
17
+ body: e2eCreateVariableBodySchema,
18
+ response: {201: e2eCreateVariableResponseSchema},
19
+ },
20
+ errorHandler: translateManagementError,
21
+ handler: async (request, reply) => {
22
+ const variable = (
23
+ await setManagedVariables({
24
+ workspaceId: request.body.workspace_id,
25
+ actorId: request.body.actor_id,
26
+ projectId: request.body.project_id,
27
+ entries: [{key: request.body.key, value: request.body.value}],
28
+ })
29
+ )[0] as ManagedVariable;
30
+
31
+ reply.code(201);
32
+ return toVariableDto(variable);
33
+ },
34
+ });