@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,94 @@
1
+ import {closeApp, createApp} from '@shipfox/node-fastify';
2
+ import {eq} from 'drizzle-orm';
3
+ import {getSecret} from '#core/index.js';
4
+ import {db, secretValues, secretVariables} from '#db/index.js';
5
+ import {secretsE2eRoutes} from './index.js';
6
+
7
+ const ACTOR_ID = '11111111-1111-4111-8111-111111111111';
8
+
9
+ describe('secrets e2e routes', () => {
10
+ let app: Awaited<ReturnType<typeof createApp>>;
11
+ let workspaceId: string;
12
+
13
+ beforeEach(async () => {
14
+ await closeApp();
15
+ workspaceId = crypto.randomUUID();
16
+ app = await createApp({routes: [secretsE2eRoutes], swagger: false});
17
+ await app.ready();
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await db().delete(secretValues).where(eq(secretValues.workspaceId, workspaceId));
22
+ await db().delete(secretVariables).where(eq(secretVariables.workspaceId, workspaceId));
23
+ await closeApp();
24
+ });
25
+
26
+ it('creates a secret setup row without exposing plaintext', async () => {
27
+ const value = 'seeded-secret-value';
28
+
29
+ const res = await app.inject({
30
+ method: 'POST',
31
+ url: '/secrets/secret',
32
+ payload: {
33
+ workspace_id: workspaceId,
34
+ actor_id: ACTOR_ID,
35
+ key: 'API_TOKEN',
36
+ value,
37
+ },
38
+ });
39
+ const stored = await getSecret({workspaceId, key: 'API_TOKEN'});
40
+
41
+ expect(res.statusCode).toBe(201);
42
+ expect(res.json()).toMatchObject({
43
+ key: 'API_TOKEN',
44
+ project_id: null,
45
+ last_edited_by: ACTOR_ID,
46
+ });
47
+ expect(stored).toBe(value);
48
+ expect(res.body).not.toContain(value);
49
+ });
50
+
51
+ it('creates a variable setup row with its readable value', async () => {
52
+ const res = await app.inject({
53
+ method: 'POST',
54
+ url: '/secrets/variable',
55
+ payload: {
56
+ workspace_id: workspaceId,
57
+ actor_id: ACTOR_ID,
58
+ key: 'REGION',
59
+ value: 'eu-west-1',
60
+ },
61
+ });
62
+
63
+ expect(res.statusCode).toBe(201);
64
+ expect(res.json()).toMatchObject({
65
+ key: 'REGION',
66
+ project_id: null,
67
+ value: 'eu-west-1',
68
+ last_edited_by: ACTOR_ID,
69
+ });
70
+ });
71
+
72
+ it('rejects invalid setup keys through route validation', async () => {
73
+ const res = await app.inject({
74
+ method: 'POST',
75
+ url: '/secrets/secret',
76
+ payload: {
77
+ workspace_id: workspaceId,
78
+ actor_id: ACTOR_ID,
79
+ key: 'bad-key',
80
+ value: 'seeded-secret-value',
81
+ },
82
+ });
83
+
84
+ expect(res.statusCode).toBe(400);
85
+ expect(res.json()).toMatchObject({code: 'validation-error'});
86
+ });
87
+
88
+ it('registers e2e setup routes without user auth', () => {
89
+ expect(secretsE2eRoutes.prefix).toBe('/secrets');
90
+ for (const route of secretsE2eRoutes.routes) {
91
+ expect(route.auth).toBeUndefined();
92
+ }
93
+ });
94
+ });
@@ -0,0 +1,8 @@
1
+ import type {RouteGroup} from '@shipfox/node-fastify';
2
+ import {createE2eSecretRoute} from './create-secret.js';
3
+ import {createE2eVariableRoute} from './create-variable.js';
4
+
5
+ export const secretsE2eRoutes: RouteGroup = {
6
+ prefix: '/secrets',
7
+ routes: [createE2eSecretRoute, createE2eVariableRoute],
8
+ };
@@ -0,0 +1,50 @@
1
+ import {requireWorkspaceAccess} from '@shipfox/api-auth-context';
2
+ import {requireProjectForWorkspace} from '@shipfox/api-projects';
3
+ import {ClientError} from '@shipfox/node-fastify';
4
+ import type {FastifyRequest} from 'fastify';
5
+
6
+ export interface ManagementAccess {
7
+ workspaceId: string;
8
+ userId: string;
9
+ }
10
+
11
+ export async function requireManagementRead(params: {
12
+ request: FastifyRequest;
13
+ workspaceId: string;
14
+ projectId?: string | undefined;
15
+ }): Promise<ManagementAccess> {
16
+ const membership = requireWorkspaceAccess({
17
+ request: params.request,
18
+ workspaceId: params.workspaceId,
19
+ });
20
+ if (params.projectId) {
21
+ await requireProjectForWorkspace({
22
+ workspaceId: params.workspaceId,
23
+ projectId: params.projectId,
24
+ });
25
+ }
26
+
27
+ return {workspaceId: params.workspaceId, userId: membership.userId};
28
+ }
29
+
30
+ export async function requireManagementWrite(params: {
31
+ request: FastifyRequest;
32
+ workspaceId: string;
33
+ projectId?: string | undefined;
34
+ }): Promise<ManagementAccess> {
35
+ const membership = requireWorkspaceAccess({
36
+ request: params.request,
37
+ workspaceId: params.workspaceId,
38
+ });
39
+ if (membership.role !== 'admin') {
40
+ throw new ClientError('Workspace admin role required', 'forbidden', {status: 403});
41
+ }
42
+ if (params.projectId) {
43
+ await requireProjectForWorkspace({
44
+ workspaceId: params.workspaceId,
45
+ projectId: params.projectId,
46
+ });
47
+ }
48
+
49
+ return {workspaceId: params.workspaceId, userId: membership.userId};
50
+ }
@@ -0,0 +1,33 @@
1
+ import {batchSecretsBodySchema, batchSecretsResponseSchema} from '@shipfox/api-secrets-dto';
2
+ import {defineRoute} from '@shipfox/node-fastify';
3
+ import {z} from 'zod';
4
+ import {setManagedSecrets} from '#core/index.js';
5
+ import {toSecretDto} from '#presentation/dto/index.js';
6
+ import {requireManagementWrite} from './auth.js';
7
+ import {translateManagementError} from './errors.js';
8
+ import {secretWarnings} from './warnings.js';
9
+
10
+ export const batchSecretsRoute = defineRoute({
11
+ method: 'POST',
12
+ path: '/secrets::batch',
13
+ description: 'Create or update multiple user-managed secrets in one scope.',
14
+ schema: {
15
+ params: z.object({workspaceId: z.string().uuid()}),
16
+ body: batchSecretsBodySchema,
17
+ response: {200: batchSecretsResponseSchema},
18
+ },
19
+ errorHandler: translateManagementError,
20
+ handler: async (request) => {
21
+ const {workspaceId} = request.params;
22
+ const {project_id: projectId, entries} = request.body;
23
+ const access = await requireManagementWrite({request, workspaceId, projectId});
24
+ const secrets = await setManagedSecrets({
25
+ workspaceId,
26
+ projectId,
27
+ actorId: access.userId,
28
+ entries,
29
+ });
30
+
31
+ return {secrets: secrets.map(toSecretDto), warnings: secretWarnings(entries)};
32
+ },
33
+ });
@@ -0,0 +1,33 @@
1
+ import {batchVariablesBodySchema, batchVariablesResponseSchema} from '@shipfox/api-secrets-dto';
2
+ import {defineRoute} from '@shipfox/node-fastify';
3
+ import {z} from 'zod';
4
+ import {setManagedVariables} from '#core/index.js';
5
+ import {toVariableDto} from '#presentation/dto/index.js';
6
+ import {requireManagementWrite} from './auth.js';
7
+ import {translateManagementError} from './errors.js';
8
+ import {variableWarnings} from './warnings.js';
9
+
10
+ export const batchVariablesRoute = defineRoute({
11
+ method: 'POST',
12
+ path: '/variables::batch',
13
+ description: 'Create or update multiple user-managed variables in one scope.',
14
+ schema: {
15
+ params: z.object({workspaceId: z.string().uuid()}),
16
+ body: batchVariablesBodySchema,
17
+ response: {200: batchVariablesResponseSchema},
18
+ },
19
+ errorHandler: translateManagementError,
20
+ handler: async (request) => {
21
+ const {workspaceId} = request.params;
22
+ const {project_id: projectId, entries} = request.body;
23
+ const access = await requireManagementWrite({request, workspaceId, projectId});
24
+ const variables = await setManagedVariables({
25
+ workspaceId,
26
+ projectId,
27
+ actorId: access.userId,
28
+ entries,
29
+ });
30
+
31
+ return {variables: variables.map(toVariableDto), warnings: variableWarnings(entries)};
32
+ },
33
+ });
@@ -0,0 +1,23 @@
1
+ interface ManagementCursor {
2
+ key: string;
3
+ }
4
+
5
+ export function encodeManagementCursor(cursor: string): string {
6
+ return Buffer.from(JSON.stringify({key: cursor} satisfies ManagementCursor), 'utf8').toString(
7
+ 'base64url',
8
+ );
9
+ }
10
+
11
+ export function decodeManagementCursor(cursor: string | undefined): string | undefined {
12
+ if (!cursor) return undefined;
13
+
14
+ try {
15
+ const parsed: unknown = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
16
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
17
+ const {key} = parsed as Partial<ManagementCursor>;
18
+ if (typeof key !== 'string' || key.length === 0) return undefined;
19
+ return key;
20
+ } catch {
21
+ return undefined;
22
+ }
23
+ }
@@ -0,0 +1,26 @@
1
+ import {secretKeySchema, secretScopeQuerySchema} from '@shipfox/api-secrets-dto';
2
+ import {defineRoute} from '@shipfox/node-fastify';
3
+ import {z} from 'zod';
4
+ import {deleteManagedSecret} from '#core/index.js';
5
+ import {requireManagementWrite} from './auth.js';
6
+ import {translateManagementError} from './errors.js';
7
+
8
+ export const deleteSecretRoute = defineRoute({
9
+ method: 'DELETE',
10
+ path: '/secrets/:key',
11
+ description: 'Delete a user-managed secret.',
12
+ schema: {
13
+ params: z.object({workspaceId: z.string().uuid(), key: secretKeySchema}),
14
+ querystring: secretScopeQuerySchema,
15
+ response: {204: z.void()},
16
+ },
17
+ errorHandler: translateManagementError,
18
+ handler: async (request, reply) => {
19
+ const {workspaceId, key} = request.params;
20
+ const {project_id: projectId} = request.query;
21
+ const access = await requireManagementWrite({request, workspaceId, projectId});
22
+
23
+ await deleteManagedSecret({workspaceId, projectId, key, actorId: access.userId});
24
+ return reply.status(204).send();
25
+ },
26
+ });
@@ -0,0 +1,26 @@
1
+ import {secretKeySchema, secretScopeQuerySchema} from '@shipfox/api-secrets-dto';
2
+ import {defineRoute} from '@shipfox/node-fastify';
3
+ import {z} from 'zod';
4
+ import {deleteManagedVariable} from '#core/index.js';
5
+ import {requireManagementWrite} from './auth.js';
6
+ import {translateManagementError} from './errors.js';
7
+
8
+ export const deleteVariableRoute = defineRoute({
9
+ method: 'DELETE',
10
+ path: '/variables/:key',
11
+ description: 'Delete a user-managed variable.',
12
+ schema: {
13
+ params: z.object({workspaceId: z.string().uuid(), key: secretKeySchema}),
14
+ querystring: secretScopeQuerySchema,
15
+ response: {204: z.void()},
16
+ },
17
+ errorHandler: translateManagementError,
18
+ handler: async (request, reply) => {
19
+ const {workspaceId, key} = request.params;
20
+ const {project_id: projectId} = request.query;
21
+ const access = await requireManagementWrite({request, workspaceId, projectId});
22
+
23
+ await deleteManagedVariable({workspaceId, projectId, key, actorId: access.userId});
24
+ return reply.status(204).send();
25
+ },
26
+ });
@@ -0,0 +1,63 @@
1
+ import {ProjectNotFoundError} from '@shipfox/api-projects';
2
+ import {ClientError} from '@shipfox/node-fastify';
3
+ import {
4
+ NamespaceValidationError,
5
+ SecretBatchDuplicateKeyError,
6
+ SecretBatchScopeMismatchError,
7
+ SecretKeyValidationError,
8
+ SecretNotFoundError,
9
+ SecretValueTooLargeError,
10
+ UnknownSecretStoreError,
11
+ VariableNotFoundError,
12
+ WorkspaceSecretCapExceededError,
13
+ } from '#core/errors.js';
14
+
15
+ export function translateManagementError(error: unknown): never {
16
+ if (error instanceof SecretKeyValidationError) {
17
+ throw new ClientError('Invalid secret key', 'invalid-key', {
18
+ status: 400,
19
+ details: {key: error.key},
20
+ });
21
+ }
22
+ if (error instanceof NamespaceValidationError) {
23
+ throw new ClientError('Invalid namespace', 'invalid-namespace', {
24
+ status: 400,
25
+ details: {namespace: error.namespace},
26
+ });
27
+ }
28
+ if (error instanceof SecretValueTooLargeError) {
29
+ throw new ClientError(error.message, 'value-too-large', {
30
+ status: 413,
31
+ details: {max_bytes: error.maxBytes},
32
+ });
33
+ }
34
+ if (error instanceof WorkspaceSecretCapExceededError) {
35
+ throw new ClientError('Workspace secret cap exceeded', 'workspace-secret-cap-exceeded', {
36
+ status: 409,
37
+ details: {cap: error.cap},
38
+ });
39
+ }
40
+ if (error instanceof SecretBatchScopeMismatchError) {
41
+ throw new ClientError(error.message, 'batch-scope-mismatch', {status: 400});
42
+ }
43
+ if (error instanceof SecretBatchDuplicateKeyError) {
44
+ throw new ClientError('Duplicate batch key', 'duplicate-key', {
45
+ status: 400,
46
+ details: {key: error.key},
47
+ });
48
+ }
49
+ if (error instanceof SecretNotFoundError) {
50
+ throw new ClientError('Secret not found', 'secret-not-found', {status: 404});
51
+ }
52
+ if (error instanceof VariableNotFoundError) {
53
+ throw new ClientError('Variable not found', 'variable-not-found', {status: 404});
54
+ }
55
+ if (error instanceof UnknownSecretStoreError) {
56
+ throw new ClientError('Unknown secret store', 'unknown-secret-store', {status: 400});
57
+ }
58
+ if (error instanceof ProjectNotFoundError) {
59
+ throw new ClientError('Project not found', 'project-not-found', {status: 404});
60
+ }
61
+
62
+ throw error;
63
+ }
@@ -0,0 +1,32 @@
1
+ import {
2
+ getVariableResponseSchema,
3
+ secretKeySchema,
4
+ secretScopeQuerySchema,
5
+ } from '@shipfox/api-secrets-dto';
6
+ import {defineRoute} from '@shipfox/node-fastify';
7
+ import {z} from 'zod';
8
+ import {getManagedVariable} from '#core/index.js';
9
+ import {toVariableDto} from '#presentation/dto/index.js';
10
+ import {requireManagementRead} from './auth.js';
11
+ import {translateManagementError} from './errors.js';
12
+
13
+ export const getVariableRoute = defineRoute({
14
+ method: 'GET',
15
+ path: '/variables/:key',
16
+ description: 'Read a user-managed variable.',
17
+ schema: {
18
+ params: z.object({workspaceId: z.string().uuid(), key: secretKeySchema}),
19
+ querystring: secretScopeQuerySchema,
20
+ response: {200: getVariableResponseSchema},
21
+ },
22
+ errorHandler: translateManagementError,
23
+ handler: async (request) => {
24
+ const {workspaceId, key} = request.params;
25
+ const {project_id: projectId} = request.query;
26
+
27
+ await requireManagementRead({request, workspaceId, projectId});
28
+ const variable = await getManagedVariable({workspaceId, projectId, key});
29
+
30
+ return {variable: toVariableDto(variable)};
31
+ },
32
+ });
@@ -0,0 +1,29 @@
1
+ import {AUTH_USER} from '@shipfox/api-auth-context';
2
+ import type {RouteGroup} from '@shipfox/node-fastify';
3
+ import {batchSecretsRoute} from './batch-secrets.js';
4
+ import {batchVariablesRoute} from './batch-variables.js';
5
+ import {deleteSecretRoute} from './delete-secret.js';
6
+ import {deleteVariableRoute} from './delete-variable.js';
7
+ import {getVariableRoute} from './get-variable.js';
8
+ import {listSecretsRoute} from './list-secrets.js';
9
+ import {listVariablesRoute} from './list-variables.js';
10
+ import {putSecretRoute} from './put-secret.js';
11
+ import {putVariableRoute} from './put-variable.js';
12
+
13
+ export const secretsRoutes: RouteGroup[] = [
14
+ {
15
+ prefix: '/workspaces/:workspaceId',
16
+ auth: AUTH_USER,
17
+ routes: [
18
+ listSecretsRoute,
19
+ putSecretRoute,
20
+ batchSecretsRoute,
21
+ deleteSecretRoute,
22
+ listVariablesRoute,
23
+ getVariableRoute,
24
+ putVariableRoute,
25
+ batchVariablesRoute,
26
+ deleteVariableRoute,
27
+ ],
28
+ },
29
+ ];
@@ -0,0 +1,41 @@
1
+ import {listSecretsQuerySchema, listSecretsResponseSchema} from '@shipfox/api-secrets-dto';
2
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
3
+ import {z} from 'zod';
4
+ import {listManagedSecrets} from '#core/index.js';
5
+ import {toSecretDto} from '#presentation/dto/index.js';
6
+ import {requireManagementRead} from './auth.js';
7
+ import {decodeManagementCursor, encodeManagementCursor} from './cursor.js';
8
+ import {translateManagementError} from './errors.js';
9
+
10
+ export const listSecretsRoute = defineRoute({
11
+ method: 'GET',
12
+ path: '/secrets',
13
+ description: 'List user-managed secrets for a workspace or project scope.',
14
+ schema: {
15
+ params: z.object({workspaceId: z.string().uuid()}),
16
+ querystring: listSecretsQuerySchema,
17
+ response: {200: listSecretsResponseSchema},
18
+ },
19
+ errorHandler: translateManagementError,
20
+ handler: async (request) => {
21
+ const {workspaceId} = request.params;
22
+ const {project_id: projectId, limit, cursor} = request.query;
23
+ const decodedCursor = decodeManagementCursor(cursor);
24
+ if (cursor && !decodedCursor) {
25
+ throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});
26
+ }
27
+
28
+ await requireManagementRead({request, workspaceId, projectId});
29
+ const result = await listManagedSecrets({
30
+ workspaceId,
31
+ projectId,
32
+ limit,
33
+ cursor: decodedCursor ?? undefined,
34
+ });
35
+
36
+ return {
37
+ secrets: result.secrets.map(toSecretDto),
38
+ next_cursor: result.nextCursor ? encodeManagementCursor(result.nextCursor) : null,
39
+ };
40
+ },
41
+ });
@@ -0,0 +1,41 @@
1
+ import {listVariablesQuerySchema, listVariablesResponseSchema} from '@shipfox/api-secrets-dto';
2
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
3
+ import {z} from 'zod';
4
+ import {listManagedVariables} from '#core/index.js';
5
+ import {toVariableListItemDto} from '#presentation/dto/index.js';
6
+ import {requireManagementRead} from './auth.js';
7
+ import {decodeManagementCursor, encodeManagementCursor} from './cursor.js';
8
+ import {translateManagementError} from './errors.js';
9
+
10
+ export const listVariablesRoute = defineRoute({
11
+ method: 'GET',
12
+ path: '/variables',
13
+ description: 'List user-managed variables for a workspace or project scope.',
14
+ schema: {
15
+ params: z.object({workspaceId: z.string().uuid()}),
16
+ querystring: listVariablesQuerySchema,
17
+ response: {200: listVariablesResponseSchema},
18
+ },
19
+ errorHandler: translateManagementError,
20
+ handler: async (request) => {
21
+ const {workspaceId} = request.params;
22
+ const {project_id: projectId, limit, cursor} = request.query;
23
+ const decodedCursor = decodeManagementCursor(cursor);
24
+ if (cursor && !decodedCursor) {
25
+ throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});
26
+ }
27
+
28
+ await requireManagementRead({request, workspaceId, projectId});
29
+ const result = await listManagedVariables({
30
+ workspaceId,
31
+ projectId,
32
+ limit,
33
+ cursor: decodedCursor ?? undefined,
34
+ });
35
+
36
+ return {
37
+ variables: result.variables.map(toVariableListItemDto),
38
+ next_cursor: result.nextCursor ? encodeManagementCursor(result.nextCursor) : null,
39
+ };
40
+ },
41
+ });