@shipfox/client-secrets 0.2.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 (114) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +4 -0
  3. package/.storybook/preview.tsx +56 -0
  4. package/.turbo/turbo-build.log +2 -0
  5. package/.turbo/turbo-type$colon$emit.log +1 -0
  6. package/.turbo/turbo-type.log +1 -0
  7. package/CHANGELOG.md +102 -0
  8. package/LICENSE +21 -0
  9. package/dist/components/copy-key.d.ts +8 -0
  10. package/dist/components/copy-key.d.ts.map +1 -0
  11. package/dist/components/copy-key.js +21 -0
  12. package/dist/components/copy-key.js.map +1 -0
  13. package/dist/components/delete-entry-dialog.d.ts +9 -0
  14. package/dist/components/delete-entry-dialog.d.ts.map +1 -0
  15. package/dist/components/delete-entry-dialog.js +66 -0
  16. package/dist/components/delete-entry-dialog.js.map +1 -0
  17. package/dist/components/form-errors.d.ts +17 -0
  18. package/dist/components/form-errors.d.ts.map +1 -0
  19. package/dist/components/form-errors.js +81 -0
  20. package/dist/components/form-errors.js.map +1 -0
  21. package/dist/components/form-shell.d.ts +14 -0
  22. package/dist/components/form-shell.d.ts.map +1 -0
  23. package/dist/components/form-shell.js +28 -0
  24. package/dist/components/form-shell.js.map +1 -0
  25. package/dist/components/secret-form.d.ts +10 -0
  26. package/dist/components/secret-form.d.ts.map +1 -0
  27. package/dist/components/secret-form.js +176 -0
  28. package/dist/components/secret-form.js.map +1 -0
  29. package/dist/components/secret-form.stories.d.ts +9 -0
  30. package/dist/components/secret-form.stories.d.ts.map +1 -0
  31. package/dist/components/secret-form.stories.js +60 -0
  32. package/dist/components/secret-form.stories.js.map +1 -0
  33. package/dist/components/store-key.d.ts +20 -0
  34. package/dist/components/store-key.d.ts.map +1 -0
  35. package/dist/components/store-key.js +25 -0
  36. package/dist/components/store-key.js.map +1 -0
  37. package/dist/components/store-section-shell.d.ts +10 -0
  38. package/dist/components/store-section-shell.d.ts.map +1 -0
  39. package/dist/components/store-section-shell.js +40 -0
  40. package/dist/components/store-section-shell.js.map +1 -0
  41. package/dist/components/variable-form.d.ts +12 -0
  42. package/dist/components/variable-form.d.ts.map +1 -0
  43. package/dist/components/variable-form.js +186 -0
  44. package/dist/components/variable-form.js.map +1 -0
  45. package/dist/components/variable-form.stories.d.ts +9 -0
  46. package/dist/components/variable-form.stories.d.ts.map +1 -0
  47. package/dist/components/variable-form.stories.js +61 -0
  48. package/dist/components/variable-form.stories.js.map +1 -0
  49. package/dist/components/workspace-secrets-section.d.ts +4 -0
  50. package/dist/components/workspace-secrets-section.d.ts.map +1 -0
  51. package/dist/components/workspace-secrets-section.js +251 -0
  52. package/dist/components/workspace-secrets-section.js.map +1 -0
  53. package/dist/components/workspace-secrets-section.stories.d.ts +13 -0
  54. package/dist/components/workspace-secrets-section.stories.d.ts.map +1 -0
  55. package/dist/components/workspace-secrets-section.stories.js +140 -0
  56. package/dist/components/workspace-secrets-section.stories.js.map +1 -0
  57. package/dist/components/workspace-variables-section.d.ts +4 -0
  58. package/dist/components/workspace-variables-section.d.ts.map +1 -0
  59. package/dist/components/workspace-variables-section.js +255 -0
  60. package/dist/components/workspace-variables-section.js.map +1 -0
  61. package/dist/components/workspace-variables-section.stories.d.ts +11 -0
  62. package/dist/components/workspace-variables-section.stories.d.ts.map +1 -0
  63. package/dist/components/workspace-variables-section.stories.js +127 -0
  64. package/dist/components/workspace-variables-section.stories.js.map +1 -0
  65. package/dist/hooks/api/create-store-api.d.ts +56 -0
  66. package/dist/hooks/api/create-store-api.d.ts.map +1 -0
  67. package/dist/hooks/api/create-store-api.js +95 -0
  68. package/dist/hooks/api/create-store-api.js.map +1 -0
  69. package/dist/hooks/api/secrets.d.ts +61 -0
  70. package/dist/hooks/api/secrets.d.ts.map +1 -0
  71. package/dist/hooks/api/secrets.js +15 -0
  72. package/dist/hooks/api/secrets.js.map +1 -0
  73. package/dist/hooks/api/variables.d.ts +88 -0
  74. package/dist/hooks/api/variables.d.ts.map +1 -0
  75. package/dist/hooks/api/variables.js +52 -0
  76. package/dist/hooks/api/variables.js.map +1 -0
  77. package/dist/index.d.ts +6 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +7 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/tsconfig.test.tsbuildinfo +1 -0
  82. package/package.json +89 -0
  83. package/src/components/copy-key.test.ts +44 -0
  84. package/src/components/copy-key.ts +21 -0
  85. package/src/components/delete-entry-dialog.tsx +57 -0
  86. package/src/components/form-errors.test.ts +67 -0
  87. package/src/components/form-errors.ts +59 -0
  88. package/src/components/form-shell.tsx +31 -0
  89. package/src/components/secret-form.stories.tsx +58 -0
  90. package/src/components/secret-form.test.tsx +37 -0
  91. package/src/components/secret-form.tsx +170 -0
  92. package/src/components/store-key.test.ts +53 -0
  93. package/src/components/store-key.ts +36 -0
  94. package/src/components/store-section-shell.tsx +32 -0
  95. package/src/components/variable-form.stories.tsx +58 -0
  96. package/src/components/variable-form.test.tsx +60 -0
  97. package/src/components/variable-form.tsx +182 -0
  98. package/src/components/workspace-secrets-section.stories.tsx +128 -0
  99. package/src/components/workspace-secrets-section.tsx +231 -0
  100. package/src/components/workspace-variables-section.stories.tsx +121 -0
  101. package/src/components/workspace-variables-section.tsx +238 -0
  102. package/src/hooks/api/create-store-api.ts +114 -0
  103. package/src/hooks/api/secrets.test.ts +157 -0
  104. package/src/hooks/api/secrets.ts +26 -0
  105. package/src/hooks/api/variables.ts +61 -0
  106. package/src/index.ts +5 -0
  107. package/test/fixtures/secrets.ts +59 -0
  108. package/test/setup.ts +3 -0
  109. package/tsconfig.build.json +9 -0
  110. package/tsconfig.build.tsbuildinfo +1 -0
  111. package/tsconfig.json +3 -0
  112. package/tsconfig.test.json +8 -0
  113. package/vercel.json +8 -0
  114. package/vitest.config.ts +75 -0
@@ -0,0 +1,114 @@
1
+ import {SECRETS_MAX_LIST_LIMIT, type SecretWriteWarningDto} from '@shipfox/api-secrets-dto';
2
+ import {apiRequest} from '@shipfox/client-api';
3
+ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
4
+
5
+ /**
6
+ * Secrets and variables share an identical management surface (list / put /
7
+ * delete under `/workspaces/:id/{resource}`); only the response envelope keys
8
+ * and DTO types differ. This factory builds the typed transport functions,
9
+ * query keys, and React Query hooks for one resource so `secrets.ts` and
10
+ * `variables.ts` stay thin instantiations. The forms and sections diverge
11
+ * (write-only vs readable) and are kept separate on purpose.
12
+ */
13
+ export interface CreateStoreApiConfig<TItem, TListResponse, TPutResponse> {
14
+ /** URL segment and query-key namespace, e.g. `'secrets'` or `'variables'`. */
15
+ resource: string;
16
+ /** Pulls the item array out of a list response. */
17
+ listItems: (response: TListResponse) => TItem[];
18
+ /** Pulls the single item out of a put response. */
19
+ putItem: (response: TPutResponse) => TItem;
20
+ }
21
+
22
+ export interface PutResult<TItem> {
23
+ item: TItem;
24
+ warnings: SecretWriteWarningDto[];
25
+ }
26
+
27
+ export function createStoreApi<
28
+ TItem,
29
+ TListResponse,
30
+ TPutBody,
31
+ TPutResponse extends {warnings: SecretWriteWarningDto[]},
32
+ >(config: CreateStoreApiConfig<TItem, TListResponse, TPutResponse>) {
33
+ const {resource, listItems, putItem} = config;
34
+
35
+ const queryKeys = {
36
+ all: [resource] as const,
37
+ list: (workspaceId: string) => [resource, 'list', workspaceId] as const,
38
+ };
39
+
40
+ // The store is a bounded set (per-workspace cap), so the UI fetches every key
41
+ // in one call rather than paginating: request the max limit and ignore the
42
+ // cursor. See SECRETS_MAX_LIST_LIMIT / the S1a list route.
43
+ async function listAll(params: {
44
+ workspaceId: string;
45
+ projectId?: string | undefined;
46
+ signal?: AbortSignal | undefined;
47
+ }): Promise<TItem[]> {
48
+ const search = new URLSearchParams({limit: String(SECRETS_MAX_LIST_LIMIT)});
49
+ if (params.projectId) search.set('project_id', params.projectId);
50
+ const response = await apiRequest<TListResponse>(
51
+ `/workspaces/${params.workspaceId}/${resource}?${search.toString()}`,
52
+ {signal: params.signal},
53
+ );
54
+ return listItems(response);
55
+ }
56
+
57
+ async function put(params: {
58
+ workspaceId: string;
59
+ key: string;
60
+ body: TPutBody;
61
+ }): Promise<PutResult<TItem>> {
62
+ const response = await apiRequest<TPutResponse>(
63
+ `/workspaces/${params.workspaceId}/${resource}/${encodeURIComponent(params.key)}`,
64
+ {method: 'PUT', body: params.body},
65
+ );
66
+ return {item: putItem(response), warnings: response.warnings};
67
+ }
68
+
69
+ async function remove(params: {
70
+ workspaceId: string;
71
+ key: string;
72
+ projectId?: string | undefined;
73
+ }): Promise<void> {
74
+ const search = params.projectId
75
+ ? `?${new URLSearchParams({project_id: params.projectId}).toString()}`
76
+ : '';
77
+ await apiRequest<void>(
78
+ `/workspaces/${params.workspaceId}/${resource}/${encodeURIComponent(params.key)}${search}`,
79
+ {method: 'DELETE'},
80
+ );
81
+ }
82
+
83
+ function useListQuery(workspaceId: string | undefined) {
84
+ return useQuery({
85
+ queryKey: workspaceId ? queryKeys.list(workspaceId) : [resource, 'list'],
86
+ enabled: Boolean(workspaceId),
87
+ queryFn: ({signal}) => listAll({workspaceId: workspaceId ?? '', signal}),
88
+ });
89
+ }
90
+
91
+ function usePutMutation() {
92
+ const queryClient = useQueryClient();
93
+ return useMutation({
94
+ mutationFn: put,
95
+ // Invalidate the whole resource namespace so the per-item detail cache
96
+ // (full variable values) is refreshed too, not just the list.
97
+ onSuccess: async () => {
98
+ await queryClient.invalidateQueries({queryKey: queryKeys.all});
99
+ },
100
+ });
101
+ }
102
+
103
+ function useDeleteMutation() {
104
+ const queryClient = useQueryClient();
105
+ return useMutation({
106
+ mutationFn: remove,
107
+ onSuccess: async () => {
108
+ await queryClient.invalidateQueries({queryKey: queryKeys.all});
109
+ },
110
+ });
111
+ }
112
+
113
+ return {queryKeys, listAll, put, remove, useListQuery, usePutMutation, useDeleteMutation};
114
+ }
@@ -0,0 +1,157 @@
1
+ import {SECRETS_MAX_LIST_LIMIT} from '@shipfox/api-secrets-dto';
2
+ import {configureApiClient} from '@shipfox/client-api';
3
+ import {
4
+ SECRETS_TEST_WORKSPACE_ID,
5
+ secret,
6
+ secretsListResponse,
7
+ variable,
8
+ variableListItem,
9
+ variablesListResponse,
10
+ } from '#test/fixtures/secrets.js';
11
+ import {deleteSecret, listSecrets, putSecret} from './secrets.js';
12
+ import {deleteVariable, getVariable, listVariables, putVariable} from './variables.js';
13
+
14
+ function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
15
+ return new Response(JSON.stringify(body), {
16
+ headers: {'content-type': 'application/json'},
17
+ status: 200,
18
+ ...init,
19
+ });
20
+ }
21
+
22
+ describe('store transport', () => {
23
+ beforeEach(() => {
24
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl: undefined});
25
+ });
26
+
27
+ test('lists the whole secret set in a single call at the max limit', async () => {
28
+ const fetchImpl = vi.fn().mockResolvedValue(jsonResponse(secretsListResponse()));
29
+ configureApiClient({fetchImpl});
30
+
31
+ const result = await listSecrets({workspaceId: SECRETS_TEST_WORKSPACE_ID});
32
+
33
+ const request = fetchImpl.mock.calls[0]?.[0] as Request;
34
+ expect(fetchImpl).toHaveBeenCalledTimes(1);
35
+ expect(result).toEqual([secret()]);
36
+ expect(request.method).toBe('GET');
37
+ expect(request.url).toBe(
38
+ `https://api.example.test/workspaces/${SECRETS_TEST_WORKSPACE_ID}/secrets?limit=${SECRETS_MAX_LIST_LIMIT}`,
39
+ );
40
+ });
41
+
42
+ test('returns an empty array when the store is empty', async () => {
43
+ const fetchImpl = vi
44
+ .fn()
45
+ .mockResolvedValue(jsonResponse(secretsListResponse({secrets: [], next_cursor: null})));
46
+ configureApiClient({fetchImpl});
47
+
48
+ const result = await listSecrets({workspaceId: SECRETS_TEST_WORKSPACE_ID});
49
+
50
+ expect(result).toEqual([]);
51
+ });
52
+
53
+ test('puts a secret, encodes the key, and passes warnings through', async () => {
54
+ let requestBody: unknown;
55
+ const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
56
+ requestBody = await (input as Request).clone().json();
57
+ return jsonResponse({
58
+ secret: secret({key: 'MY_TOKEN'}),
59
+ warnings: [{code: 'short-secret-value', key: 'MY_TOKEN'}],
60
+ });
61
+ });
62
+ configureApiClient({fetchImpl});
63
+
64
+ const result = await putSecret({
65
+ workspaceId: SECRETS_TEST_WORKSPACE_ID,
66
+ key: 'MY_TOKEN',
67
+ body: {value: 'short'},
68
+ });
69
+
70
+ const request = fetchImpl.mock.calls[0]?.[0] as Request;
71
+ expect(request.method).toBe('PUT');
72
+ expect(request.url).toBe(
73
+ `https://api.example.test/workspaces/${SECRETS_TEST_WORKSPACE_ID}/secrets/MY_TOKEN`,
74
+ );
75
+ expect(requestBody).toEqual({value: 'short'});
76
+ expect(result.warnings).toEqual([{code: 'short-secret-value', key: 'MY_TOKEN'}]);
77
+ expect(result.item.key).toBe('MY_TOKEN');
78
+ });
79
+
80
+ test('deletes a secret with a 204 and no project scope by default', async () => {
81
+ const fetchImpl = vi.fn().mockResolvedValue(new Response(null, {status: 204}));
82
+ configureApiClient({fetchImpl});
83
+
84
+ const result = await deleteSecret({workspaceId: SECRETS_TEST_WORKSPACE_ID, key: 'MY_TOKEN'});
85
+
86
+ const request = fetchImpl.mock.calls[0]?.[0] as Request;
87
+ expect(result).toBeUndefined();
88
+ expect(request.method).toBe('DELETE');
89
+ expect(request.url).toBe(
90
+ `https://api.example.test/workspaces/${SECRETS_TEST_WORKSPACE_ID}/secrets/MY_TOKEN`,
91
+ );
92
+ });
93
+
94
+ test('lists variables (values included) via the same factory', async () => {
95
+ const fetchImpl = vi.fn().mockResolvedValue(jsonResponse(variablesListResponse()));
96
+ configureApiClient({fetchImpl});
97
+
98
+ const result = await listVariables({workspaceId: SECRETS_TEST_WORKSPACE_ID});
99
+
100
+ const request = fetchImpl.mock.calls[0]?.[0] as Request;
101
+ expect(result).toEqual([variableListItem()]);
102
+ expect(request.url).toBe(
103
+ `https://api.example.test/workspaces/${SECRETS_TEST_WORKSPACE_ID}/variables?limit=${SECRETS_MAX_LIST_LIMIT}`,
104
+ );
105
+ });
106
+
107
+ test('reads a single variable with its full value for editing', async () => {
108
+ const fetchImpl = vi
109
+ .fn()
110
+ .mockResolvedValue(jsonResponse({variable: variable({value: 'first line\nsecond line'})}));
111
+ configureApiClient({fetchImpl});
112
+
113
+ const result = await getVariable({workspaceId: SECRETS_TEST_WORKSPACE_ID, key: 'LOG_LEVEL'});
114
+
115
+ const request = fetchImpl.mock.calls[0]?.[0] as Request;
116
+ expect(result.value).toBe('first line\nsecond line');
117
+ expect(request.method).toBe('GET');
118
+ expect(request.url).toBe(
119
+ `https://api.example.test/workspaces/${SECRETS_TEST_WORKSPACE_ID}/variables/LOG_LEVEL`,
120
+ );
121
+ });
122
+
123
+ test('puts a variable and returns the sensitive-name warning', async () => {
124
+ const fetchImpl = vi.fn().mockResolvedValue(
125
+ jsonResponse({
126
+ variable: variable({key: 'MY_KEY', value: 'v'}),
127
+ warnings: [{code: 'sensitive-variable-name', key: 'MY_KEY'}],
128
+ }),
129
+ );
130
+ configureApiClient({fetchImpl});
131
+
132
+ const result = await putVariable({
133
+ workspaceId: SECRETS_TEST_WORKSPACE_ID,
134
+ key: 'MY_KEY',
135
+ body: {value: 'v'},
136
+ });
137
+
138
+ expect(result.warnings).toEqual([{code: 'sensitive-variable-name', key: 'MY_KEY'}]);
139
+ expect(result.item.value).toBe('v');
140
+ });
141
+
142
+ test('deletes a variable within a project scope via project_id query', async () => {
143
+ const fetchImpl = vi.fn().mockResolvedValue(new Response(null, {status: 204}));
144
+ configureApiClient({fetchImpl});
145
+
146
+ await deleteVariable({
147
+ workspaceId: SECRETS_TEST_WORKSPACE_ID,
148
+ key: 'LOG_LEVEL',
149
+ projectId: '33333333-3333-4333-8333-333333333333',
150
+ });
151
+
152
+ const request = fetchImpl.mock.calls[0]?.[0] as Request;
153
+ expect(request.url).toBe(
154
+ `https://api.example.test/workspaces/${SECRETS_TEST_WORKSPACE_ID}/variables/LOG_LEVEL?project_id=33333333-3333-4333-8333-333333333333`,
155
+ );
156
+ });
157
+ });
@@ -0,0 +1,26 @@
1
+ import type {
2
+ ListSecretsResponseDto,
3
+ PutSecretBodyDto,
4
+ PutSecretResponseDto,
5
+ SecretDto,
6
+ } from '@shipfox/api-secrets-dto';
7
+ import {createStoreApi} from './create-store-api.js';
8
+
9
+ const secretsApi = createStoreApi<
10
+ SecretDto,
11
+ ListSecretsResponseDto,
12
+ PutSecretBodyDto,
13
+ PutSecretResponseDto
14
+ >({
15
+ resource: 'secrets',
16
+ listItems: (response) => response.secrets,
17
+ putItem: (response) => response.secret,
18
+ });
19
+
20
+ export const secretsQueryKeys = secretsApi.queryKeys;
21
+ export const listSecrets = secretsApi.listAll;
22
+ export const putSecret = secretsApi.put;
23
+ export const deleteSecret = secretsApi.remove;
24
+ export const useSecretsQuery = secretsApi.useListQuery;
25
+ export const usePutSecretMutation = secretsApi.usePutMutation;
26
+ export const useDeleteSecretMutation = secretsApi.useDeleteMutation;
@@ -0,0 +1,61 @@
1
+ import type {
2
+ GetVariableResponseDto,
3
+ ListVariablesResponseDto,
4
+ PutVariableBodyDto,
5
+ PutVariableResponseDto,
6
+ VariableDto,
7
+ VariableListItemDto,
8
+ } from '@shipfox/api-secrets-dto';
9
+ import {apiRequest} from '@shipfox/client-api';
10
+ import {useQuery} from '@tanstack/react-query';
11
+ import {createStoreApi} from './create-store-api.js';
12
+
13
+ const variablesApi = createStoreApi<
14
+ VariableListItemDto,
15
+ ListVariablesResponseDto,
16
+ PutVariableBodyDto,
17
+ PutVariableResponseDto
18
+ >({
19
+ resource: 'variables',
20
+ listItems: (response) => response.variables,
21
+ // The list carries only a preview; a freshly-written value is the full value.
22
+ putItem: (response) => ({...response.variable, value_truncated: false}),
23
+ });
24
+
25
+ export const variablesQueryKeys = variablesApi.queryKeys;
26
+ export const listVariables = variablesApi.listAll;
27
+ export const putVariable = variablesApi.put;
28
+ export const deleteVariable = variablesApi.remove;
29
+ export const useVariablesQuery = variablesApi.useListQuery;
30
+ export const usePutVariableMutation = variablesApi.usePutMutation;
31
+ export const useDeleteVariableMutation = variablesApi.useDeleteMutation;
32
+
33
+ /**
34
+ * Reads a single variable including its full value. The list only returns a
35
+ * bounded preview, so editing a truncated variable fetches the full value here.
36
+ */
37
+ export async function getVariable(params: {
38
+ workspaceId: string;
39
+ key: string;
40
+ projectId?: string | undefined;
41
+ signal?: AbortSignal | undefined;
42
+ }): Promise<VariableDto> {
43
+ const search = params.projectId
44
+ ? `?${new URLSearchParams({project_id: params.projectId}).toString()}`
45
+ : '';
46
+ const response = await apiRequest<GetVariableResponseDto>(
47
+ `/workspaces/${params.workspaceId}/variables/${encodeURIComponent(params.key)}${search}`,
48
+ {signal: params.signal},
49
+ );
50
+ return response.variable;
51
+ }
52
+
53
+ export function useVariableQuery(workspaceId: string, key: string | undefined) {
54
+ return useQuery({
55
+ queryKey: key
56
+ ? [...variablesQueryKeys.all, 'detail', workspaceId, key]
57
+ : [...variablesQueryKeys.all, 'detail'],
58
+ enabled: Boolean(key),
59
+ queryFn: ({signal}) => getVariable({workspaceId, key: key ?? '', signal}),
60
+ });
61
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export {type SecretsFormErrorMapping, secretsErrorToFormError} from './components/form-errors.js';
2
+ export {WorkspaceSecretsSection} from './components/workspace-secrets-section.js';
3
+ export {WorkspaceVariablesSection} from './components/workspace-variables-section.js';
4
+ export * from './hooks/api/secrets.js';
5
+ export * from './hooks/api/variables.js';
@@ -0,0 +1,59 @@
1
+ import type {
2
+ ListSecretsResponseDto,
3
+ ListVariablesResponseDto,
4
+ SecretDto,
5
+ VariableDto,
6
+ VariableListItemDto,
7
+ } from '@shipfox/api-secrets-dto';
8
+
9
+ export const SECRETS_TEST_WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
10
+
11
+ export function secret(overrides: Partial<SecretDto> = {}): SecretDto {
12
+ return {
13
+ key: 'MY_TOKEN',
14
+ project_id: null,
15
+ created_at: '2026-05-08T00:00:00.000Z',
16
+ updated_at: '2026-05-08T00:00:00.000Z',
17
+ last_edited_by: '22222222-2222-4222-8222-222222222222',
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ export function variable(overrides: Partial<VariableDto> = {}): VariableDto {
23
+ return {
24
+ ...secret(),
25
+ key: 'LOG_LEVEL',
26
+ value: 'debug',
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ export function variableListItem(
32
+ overrides: Partial<VariableListItemDto> = {},
33
+ ): VariableListItemDto {
34
+ return {
35
+ ...variable(),
36
+ value_truncated: false,
37
+ ...overrides,
38
+ };
39
+ }
40
+
41
+ export function secretsListResponse(
42
+ overrides: Partial<ListSecretsResponseDto> = {},
43
+ ): ListSecretsResponseDto {
44
+ return {
45
+ secrets: [secret()],
46
+ next_cursor: null,
47
+ ...overrides,
48
+ };
49
+ }
50
+
51
+ export function variablesListResponse(
52
+ overrides: Partial<ListVariablesResponseDto> = {},
53
+ ): ListVariablesResponseDto {
54
+ return {
55
+ variables: [variableListItem()],
56
+ next_cursor: null,
57
+ ...overrides,
58
+ };
59
+ }
package/test/setup.ts ADDED
@@ -0,0 +1,3 @@
1
+ import {installClientDomTestEnv} from '@shipfox/client-test-setup';
2
+
3
+ installClientDomTestEnv();
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@shipfox/ts-config/react",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["**/*.test.tsx", "**/*.test.ts"]
9
+ }