@shipfox/client-secrets 5.0.0 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +38 -0
- package/dist/components/form-errors.d.ts.map +1 -1
- package/dist/components/form-errors.js +0 -6
- package/dist/components/form-errors.js.map +1 -1
- package/dist/components/secret-form.d.ts.map +1 -1
- package/dist/components/secret-form.js +6 -8
- package/dist/components/secret-form.js.map +1 -1
- package/dist/components/variable-form.d.ts.map +1 -1
- package/dist/components/variable-form.js +6 -8
- package/dist/components/variable-form.js.map +1 -1
- package/dist/components/workspace-secrets-section.d.ts.map +1 -1
- package/dist/components/workspace-secrets-section.js +4 -2
- package/dist/components/workspace-secrets-section.js.map +1 -1
- package/dist/components/workspace-secrets-section.stories.d.ts.map +1 -1
- package/dist/components/workspace-secrets-section.stories.js.map +1 -1
- package/dist/components/workspace-variables-section.d.ts.map +1 -1
- package/dist/components/workspace-variables-section.js +5 -3
- package/dist/components/workspace-variables-section.js.map +1 -1
- package/dist/components/workspace-variables-section.stories.d.ts.map +1 -1
- package/dist/components/workspace-variables-section.stories.js.map +1 -1
- package/dist/core/store.d.ts +54 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +44 -0
- package/dist/core/store.js.map +1 -0
- package/dist/feature.d.ts +13 -0
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +17 -16
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/create-store-api.d.ts +31 -49
- package/dist/hooks/api/create-store-api.d.ts.map +1 -1
- package/dist/hooks/api/create-store-api.js +44 -45
- package/dist/hooks/api/create-store-api.js.map +1 -1
- package/dist/hooks/api/secrets.d.ts +16 -55
- package/dist/hooks/api/secrets.d.ts.map +1 -1
- package/dist/hooks/api/secrets.js +10 -2
- package/dist/hooks/api/secrets.js.map +1 -1
- package/dist/hooks/api/store-mapper.d.ts +7 -0
- package/dist/hooks/api/store-mapper.d.ts.map +1 -0
- package/dist/hooks/api/store-mapper.js +36 -0
- package/dist/hooks/api/store-mapper.js.map +1 -0
- package/dist/hooks/api/variables.d.ts +25 -78
- package/dist/hooks/api/variables.d.ts.map +1 -1
- package/dist/hooks/api/variables.js +37 -28
- package/dist/hooks/api/variables.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +9 -10
- package/src/components/form-errors.test.ts +2 -2
- package/src/components/form-errors.ts +0 -3
- package/src/components/secret-form.tsx +15 -5
- package/src/components/variable-form.tsx +15 -5
- package/src/components/workspace-secrets-section.stories.tsx +1 -2
- package/src/components/workspace-secrets-section.tsx +8 -4
- package/src/components/workspace-variables-section.stories.tsx +1 -2
- package/src/components/workspace-variables-section.tsx +10 -6
- package/src/core/store.test.ts +38 -0
- package/src/core/store.ts +91 -0
- package/src/feature.ts +13 -11
- package/src/hooks/api/create-store-api.ts +104 -75
- package/src/hooks/api/secrets.test.ts +16 -10
- package/src/hooks/api/secrets.ts +11 -14
- package/src/hooks/api/store-mapper.test.ts +39 -0
- package/src/hooks/api/store-mapper.ts +47 -0
- package/src/hooks/api/variables.ts +52 -36
- package/tsconfig.build.tsbuildinfo +1 -1
- package/vercel.json +5 -0
- package/dist/components/store-key.d.ts +0 -20
- package/dist/components/store-key.d.ts.map +0 -1
- package/dist/components/store-key.js +0 -25
- package/dist/components/store-key.js.map +0 -1
- package/src/components/store-key.test.ts +0 -53
- package/src/components/store-key.ts +0 -36
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-secrets",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "6.0.1",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -10,10 +10,6 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"main": "dist/index.js",
|
|
12
12
|
"types": "dist/index.d.ts",
|
|
13
|
-
"imports": {
|
|
14
|
-
"#*": "./dist/*",
|
|
15
|
-
"#test/*": "./test/*"
|
|
16
|
-
},
|
|
17
13
|
"exports": {
|
|
18
14
|
".": {
|
|
19
15
|
"types": "./dist/index.d.ts",
|
|
@@ -31,17 +27,20 @@
|
|
|
31
27
|
"dependencies": {
|
|
32
28
|
"@swc/helpers": "^0.5.17",
|
|
33
29
|
"@tanstack/react-form": "^1.32.0",
|
|
34
|
-
"@shipfox/api-secrets-dto": "
|
|
35
|
-
"@shipfox/
|
|
36
|
-
"@shipfox/client-shell": "
|
|
37
|
-
"@shipfox/client-ui": "
|
|
38
|
-
"@shipfox/
|
|
30
|
+
"@shipfox/api-secrets-dto": "9.0.1",
|
|
31
|
+
"@shipfox/react-ui": "0.3.6",
|
|
32
|
+
"@shipfox/client-shell": "6.0.1",
|
|
33
|
+
"@shipfox/client-ui": "6.0.1",
|
|
34
|
+
"@shipfox/client-api": "6.0.1"
|
|
39
35
|
},
|
|
40
36
|
"peerDependencies": {
|
|
41
37
|
"@tanstack/react-query": "^5.101.0",
|
|
42
38
|
"react": "^19.0.0",
|
|
43
39
|
"react-dom": "^19.0.0"
|
|
44
40
|
},
|
|
41
|
+
"imports": {
|
|
42
|
+
"#*": "./dist/*"
|
|
43
|
+
},
|
|
45
44
|
"scripts": {
|
|
46
45
|
"build": "shipfox-swc",
|
|
47
46
|
"check": "shipfox-biome-check",
|
|
@@ -53,10 +53,10 @@ describe('secretsErrorToFormError', () => {
|
|
|
53
53
|
expect(result).toEqual({kind: 'form', message: 'raw server message'});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
test('
|
|
56
|
+
test('does not expose a raw Error message', () => {
|
|
57
57
|
const result = secretsErrorToFormError(new Error('boom'));
|
|
58
58
|
|
|
59
|
-
expect(result).toEqual({kind: 'form', message: '
|
|
59
|
+
expect(result).toEqual({kind: 'form', message: 'Something went wrong. Try again.'});
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
test('falls back to generic copy for a non-Error throwable', () => {
|
|
@@ -52,8 +52,5 @@ export function secretsErrorToFormError(error: unknown): SecretsFormErrorMapping
|
|
|
52
52
|
return {kind: 'form', message: error.message};
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
if (error instanceof Error) {
|
|
56
|
-
return {kind: 'form', message: error.message};
|
|
57
|
-
}
|
|
58
55
|
return {kind: 'form', message: 'Something went wrong. Try again.'};
|
|
59
56
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {isShortSecretValue} from '@shipfox/api-secrets-dto';
|
|
2
1
|
import {Button, IconButton} from '@shipfox/react-ui/button';
|
|
3
2
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
4
3
|
import {
|
|
@@ -10,10 +9,16 @@ import {
|
|
|
10
9
|
import {Text} from '@shipfox/react-ui/typography';
|
|
11
10
|
import {useForm} from '@tanstack/react-form';
|
|
12
11
|
import {useState} from 'react';
|
|
12
|
+
import {
|
|
13
|
+
normalizeStoreKey,
|
|
14
|
+
STORE_KEY_HELP,
|
|
15
|
+
shouldWarnShortSecretValue,
|
|
16
|
+
validateNewStoreKey,
|
|
17
|
+
workspaceStoreScope,
|
|
18
|
+
} from '#core/store.js';
|
|
13
19
|
import {usePutSecretMutation} from '#hooks/api/secrets.js';
|
|
14
20
|
import {secretsErrorToFormError} from './form-errors.js';
|
|
15
21
|
import {FormBody, FormFooter} from './form-shell.js';
|
|
16
|
-
import {STORE_KEY_HELP, validateNewStoreKey} from './store-key.js';
|
|
17
22
|
|
|
18
23
|
export const SECRET_FORM_ID = 'secret-form';
|
|
19
24
|
|
|
@@ -45,7 +50,12 @@ export function SecretForm({
|
|
|
45
50
|
onSubmit: async ({value}) => {
|
|
46
51
|
setFormError(undefined);
|
|
47
52
|
try {
|
|
48
|
-
await putSecret.mutateAsync({
|
|
53
|
+
await putSecret.mutateAsync({
|
|
54
|
+
workspaceId,
|
|
55
|
+
key: normalizeStoreKey(value.key),
|
|
56
|
+
value: value.value,
|
|
57
|
+
scope: workspaceStoreScope,
|
|
58
|
+
});
|
|
49
59
|
onSaved();
|
|
50
60
|
} catch (error) {
|
|
51
61
|
const mapped = secretsErrorToFormError(error);
|
|
@@ -96,7 +106,7 @@ export function SecretForm({
|
|
|
96
106
|
disabled={mode === 'edit'}
|
|
97
107
|
placeholder="MY_TOKEN"
|
|
98
108
|
value={field.state.value}
|
|
99
|
-
onChange={(event) => field.handleChange(event.target.value
|
|
109
|
+
onChange={(event) => field.handleChange(normalizeStoreKey(event.target.value))}
|
|
100
110
|
onBlur={field.handleBlur}
|
|
101
111
|
/>
|
|
102
112
|
</FormField>
|
|
@@ -139,7 +149,7 @@ export function SecretForm({
|
|
|
139
149
|
onClick={() => setShowValue((prev) => !prev)}
|
|
140
150
|
/>
|
|
141
151
|
</div>
|
|
142
|
-
{
|
|
152
|
+
{shouldWarnShortSecretValue(field.state.value, SHORT_VALUE_THRESHOLD) ? (
|
|
143
153
|
<Callout role="alert" type="warning">
|
|
144
154
|
<Text size="sm">
|
|
145
155
|
Very short secrets can match ordinary log text and redact unrelated output. If
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {isSensitiveSecretName} from '@shipfox/api-secrets-dto';
|
|
2
1
|
import {Button} from '@shipfox/react-ui/button';
|
|
3
2
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
4
3
|
import {
|
|
@@ -10,10 +9,16 @@ import {
|
|
|
10
9
|
import {Text} from '@shipfox/react-ui/typography';
|
|
11
10
|
import {useForm} from '@tanstack/react-form';
|
|
12
11
|
import {useEffect, useRef, useState} from 'react';
|
|
12
|
+
import {
|
|
13
|
+
normalizeStoreKey,
|
|
14
|
+
STORE_KEY_HELP,
|
|
15
|
+
shouldWarnSensitiveVariableName,
|
|
16
|
+
validateNewStoreKey,
|
|
17
|
+
workspaceStoreScope,
|
|
18
|
+
} from '#core/store.js';
|
|
13
19
|
import {usePutVariableMutation, useVariableQuery} from '#hooks/api/variables.js';
|
|
14
20
|
import {secretsErrorToFormError} from './form-errors.js';
|
|
15
21
|
import {FormBody, FormFooter} from './form-shell.js';
|
|
16
|
-
import {STORE_KEY_HELP, validateNewStoreKey} from './store-key.js';
|
|
17
22
|
|
|
18
23
|
export const VARIABLE_FORM_ID = 'variable-form';
|
|
19
24
|
|
|
@@ -49,7 +54,12 @@ export function VariableForm({
|
|
|
49
54
|
onSubmit: async ({value}) => {
|
|
50
55
|
setFormError(undefined);
|
|
51
56
|
try {
|
|
52
|
-
await putVariable.mutateAsync({
|
|
57
|
+
await putVariable.mutateAsync({
|
|
58
|
+
workspaceId,
|
|
59
|
+
key: normalizeStoreKey(value.key),
|
|
60
|
+
value: value.value,
|
|
61
|
+
scope: workspaceStoreScope,
|
|
62
|
+
});
|
|
53
63
|
onSaved();
|
|
54
64
|
} catch (error) {
|
|
55
65
|
const mapped = secretsErrorToFormError(error);
|
|
@@ -115,10 +125,10 @@ export function VariableForm({
|
|
|
115
125
|
disabled={mode === 'edit'}
|
|
116
126
|
placeholder="LOG_LEVEL"
|
|
117
127
|
value={field.state.value}
|
|
118
|
-
onChange={(event) => field.handleChange(event.target.value
|
|
128
|
+
onChange={(event) => field.handleChange(normalizeStoreKey(event.target.value))}
|
|
119
129
|
onBlur={field.handleBlur}
|
|
120
130
|
/>
|
|
121
|
-
{
|
|
131
|
+
{shouldWarnSensitiveVariableName(field.state.value) ? (
|
|
122
132
|
<Callout role="alert" type="warning">
|
|
123
133
|
<Text size="sm" aria-live="polite">
|
|
124
134
|
This looks like it may be sensitive. Variables are stored in plaintext and are
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type {SecretDto} from '@shipfox/api-secrets-dto';
|
|
2
1
|
import {configureApiClient} from '@shipfox/client-api';
|
|
3
2
|
import {Toaster} from '@shipfox/react-ui/toast';
|
|
4
3
|
import type {Decorator, Meta, StoryObj} from '@storybook/react';
|
|
@@ -25,7 +24,7 @@ const withFrozenClock: Decorator = (Story) => {
|
|
|
25
24
|
const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
|
|
26
25
|
const EDITOR_ID = '22222222-2222-4222-8222-222222222222';
|
|
27
26
|
|
|
28
|
-
const SECRETS
|
|
27
|
+
const SECRETS = [
|
|
29
28
|
{
|
|
30
29
|
key: 'API_TOKEN',
|
|
31
30
|
project_id: null,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type {SecretDto} from '@shipfox/api-secrets-dto';
|
|
2
1
|
import {QueryLoadError} from '@shipfox/client-ui';
|
|
3
2
|
import {Button, IconButton} from '@shipfox/react-ui/button';
|
|
4
3
|
import {
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
import {toast} from '@shipfox/react-ui/toast';
|
|
22
21
|
import {Code, Header, Text} from '@shipfox/react-ui/typography';
|
|
23
22
|
import {useMemo, useState} from 'react';
|
|
23
|
+
import {type SecretMetadata, workspaceStoreScope} from '#core/store.js';
|
|
24
24
|
import {useDeleteSecretMutation, useSecretsQuery} from '#hooks/api/secrets.js';
|
|
25
25
|
import {copyKeyName} from './copy-key.js';
|
|
26
26
|
import {DeleteEntryDialog} from './delete-entry-dialog.js';
|
|
@@ -159,7 +159,11 @@ export function WorkspaceSecretsSection({workspaceId}: {workspaceId: string}) {
|
|
|
159
159
|
if (deleteKey === null) return;
|
|
160
160
|
setDeleteError(undefined);
|
|
161
161
|
try {
|
|
162
|
-
await deleteSecret.mutateAsync({
|
|
162
|
+
await deleteSecret.mutateAsync({
|
|
163
|
+
workspaceId,
|
|
164
|
+
key: deleteKey,
|
|
165
|
+
scope: workspaceStoreScope,
|
|
166
|
+
});
|
|
163
167
|
toast.success('Secret deleted');
|
|
164
168
|
closeDelete();
|
|
165
169
|
} catch (error) {
|
|
@@ -176,7 +180,7 @@ function SecretRow({
|
|
|
176
180
|
onEdit,
|
|
177
181
|
onDelete,
|
|
178
182
|
}: {
|
|
179
|
-
secret:
|
|
183
|
+
secret: SecretMetadata;
|
|
180
184
|
onEdit: () => void;
|
|
181
185
|
onDelete: () => void;
|
|
182
186
|
}) {
|
|
@@ -204,7 +208,7 @@ function SecretRow({
|
|
|
204
208
|
</span>
|
|
205
209
|
</TableCell>
|
|
206
210
|
<TableCell className="text-foreground-neutral-muted">
|
|
207
|
-
<RelativeTime value={secret.
|
|
211
|
+
<RelativeTime value={secret.updatedAt} />
|
|
208
212
|
</TableCell>
|
|
209
213
|
<TableCell className="text-right">
|
|
210
214
|
<DropdownMenu>
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type {VariableListItemDto} from '@shipfox/api-secrets-dto';
|
|
2
1
|
import {configureApiClient} from '@shipfox/client-api';
|
|
3
2
|
import {Toaster} from '@shipfox/react-ui/toast';
|
|
4
3
|
import type {Decorator, Meta, StoryObj} from '@storybook/react';
|
|
@@ -25,7 +24,7 @@ const withFrozenClock: Decorator = (Story) => {
|
|
|
25
24
|
const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
|
|
26
25
|
const EDITOR_ID = '22222222-2222-4222-8222-222222222222';
|
|
27
26
|
|
|
28
|
-
const VARIABLES
|
|
27
|
+
const VARIABLES = [
|
|
29
28
|
{
|
|
30
29
|
key: 'LOG_LEVEL',
|
|
31
30
|
value: 'debug',
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type {VariableListItemDto} from '@shipfox/api-secrets-dto';
|
|
2
1
|
import {QueryLoadError} from '@shipfox/client-ui';
|
|
3
2
|
import {Button, IconButton} from '@shipfox/react-ui/button';
|
|
4
3
|
import {
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
import {toast} from '@shipfox/react-ui/toast';
|
|
22
21
|
import {Code, Header, Text} from '@shipfox/react-ui/typography';
|
|
23
22
|
import {useMemo, useState} from 'react';
|
|
23
|
+
import {type VariablePreview, workspaceStoreScope} from '#core/store.js';
|
|
24
24
|
import {useDeleteVariableMutation, useVariablesQuery} from '#hooks/api/variables.js';
|
|
25
25
|
import {copyKeyName} from './copy-key.js';
|
|
26
26
|
import {DeleteEntryDialog} from './delete-entry-dialog.js';
|
|
@@ -33,7 +33,7 @@ const VARIABLES_DESCRIPTION =
|
|
|
33
33
|
const EMPTY_VARIABLES_DESCRIPTION =
|
|
34
34
|
'Create a variable to store non-sensitive configuration like regions, flags, and log levels.';
|
|
35
35
|
|
|
36
|
-
type FormState = {mode: 'create'} | {mode: 'edit'; variable:
|
|
36
|
+
type FormState = {mode: 'create'} | {mode: 'edit'; variable: VariablePreview} | null;
|
|
37
37
|
|
|
38
38
|
export function WorkspaceVariablesSection({workspaceId}: {workspaceId: string}) {
|
|
39
39
|
const variablesQuery = useVariablesQuery(workspaceId);
|
|
@@ -137,7 +137,7 @@ export function WorkspaceVariablesSection({workspaceId}: {workspaceId: string})
|
|
|
137
137
|
existingKey={formState.mode === 'edit' ? formState.variable.key : undefined}
|
|
138
138
|
existingValue={formState.mode === 'edit' ? formState.variable.value : undefined}
|
|
139
139
|
existingValueTruncated={
|
|
140
|
-
formState.mode === 'edit' ? formState.variable.
|
|
140
|
+
formState.mode === 'edit' ? formState.variable.valueTruncated : undefined
|
|
141
141
|
}
|
|
142
142
|
reservedKeys={variables.map((variable) => variable.key)}
|
|
143
143
|
onSaved={() => {
|
|
@@ -163,7 +163,11 @@ export function WorkspaceVariablesSection({workspaceId}: {workspaceId: string})
|
|
|
163
163
|
if (deleteKey === null) return;
|
|
164
164
|
setDeleteError(undefined);
|
|
165
165
|
try {
|
|
166
|
-
await deleteVariable.mutateAsync({
|
|
166
|
+
await deleteVariable.mutateAsync({
|
|
167
|
+
workspaceId,
|
|
168
|
+
key: deleteKey,
|
|
169
|
+
scope: workspaceStoreScope,
|
|
170
|
+
});
|
|
167
171
|
toast.success('Variable deleted');
|
|
168
172
|
closeDelete();
|
|
169
173
|
} catch (error) {
|
|
@@ -180,7 +184,7 @@ function VariableRow({
|
|
|
180
184
|
onEdit,
|
|
181
185
|
onDelete,
|
|
182
186
|
}: {
|
|
183
|
-
variable:
|
|
187
|
+
variable: VariablePreview;
|
|
184
188
|
onEdit: () => void;
|
|
185
189
|
onDelete: () => void;
|
|
186
190
|
}) {
|
|
@@ -211,7 +215,7 @@ function VariableRow({
|
|
|
211
215
|
</span>
|
|
212
216
|
</TableCell>
|
|
213
217
|
<TableCell className="text-foreground-neutral-muted">
|
|
214
|
-
<RelativeTime value={variable.
|
|
218
|
+
<RelativeTime value={variable.updatedAt} />
|
|
215
219
|
</TableCell>
|
|
216
220
|
<TableCell className="text-right">
|
|
217
221
|
<DropdownMenu>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeStoreKey,
|
|
3
|
+
STORE_KEY_HELP,
|
|
4
|
+
shouldWarnSensitiveVariableName,
|
|
5
|
+
shouldWarnShortSecretValue,
|
|
6
|
+
validateNewStoreKey,
|
|
7
|
+
validateStoreKey,
|
|
8
|
+
} from './store.js';
|
|
9
|
+
|
|
10
|
+
describe('store policy', () => {
|
|
11
|
+
test('normalizes keys and enforces the store key format', () => {
|
|
12
|
+
expect(normalizeStoreKey('my_token')).toBe('MY_TOKEN');
|
|
13
|
+
expect(validateStoreKey('MY_TOKEN')).toBeUndefined();
|
|
14
|
+
expect(validateStoreKey('my token')).toBe(STORE_KEY_HELP);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('prevents create mode from overwriting an existing write-only secret', () => {
|
|
18
|
+
expect(
|
|
19
|
+
validateNewStoreKey('API_TOKEN', {
|
|
20
|
+
mode: 'create',
|
|
21
|
+
reservedKeys: ['API_TOKEN'],
|
|
22
|
+
kind: 'secret',
|
|
23
|
+
}),
|
|
24
|
+
).toBe('A secret with this name already exists. Edit it instead.');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('keeps edit mode and warning interpretation explicit', () => {
|
|
28
|
+
expect(
|
|
29
|
+
validateNewStoreKey('API_TOKEN', {
|
|
30
|
+
mode: 'edit',
|
|
31
|
+
reservedKeys: ['API_TOKEN'],
|
|
32
|
+
kind: 'secret',
|
|
33
|
+
}),
|
|
34
|
+
).toBeUndefined();
|
|
35
|
+
expect(shouldWarnShortSecretValue('short', 12)).toBe(true);
|
|
36
|
+
expect(shouldWarnSensitiveVariableName('API_TOKEN')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const STORE_KEY_HELP =
|
|
2
|
+
'Uppercase letters, digits and underscores; must start with a letter or underscore.';
|
|
3
|
+
|
|
4
|
+
// This policy intentionally lives here so the package core stays independent of transport DTOs.
|
|
5
|
+
const STORE_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
6
|
+
const STORE_KEY_MAX_LENGTH = 128;
|
|
7
|
+
const SENSITIVE_NAME_PATTERNS = ['TOKEN', 'SECRET', 'PASSWORD', 'KEY'];
|
|
8
|
+
|
|
9
|
+
export type StoreScope = {kind: 'workspace'} | {kind: 'project'; projectId: string};
|
|
10
|
+
|
|
11
|
+
export const workspaceStoreScope: StoreScope = {kind: 'workspace'};
|
|
12
|
+
|
|
13
|
+
export interface StoreMetadata {
|
|
14
|
+
key: string;
|
|
15
|
+
scope: StoreScope;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
updatedAt: string;
|
|
18
|
+
lastEditedBy: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SecretMetadata = StoreMetadata;
|
|
22
|
+
|
|
23
|
+
export interface VariablePreview extends StoreMetadata {
|
|
24
|
+
value: string;
|
|
25
|
+
valueState: 'preview';
|
|
26
|
+
valueTruncated: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Variable extends StoreMetadata {
|
|
30
|
+
value: string;
|
|
31
|
+
valueState: 'full';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StoreWriteWarning {
|
|
35
|
+
code: 'short-secret-value' | 'sensitive-variable-name';
|
|
36
|
+
key: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PutStoreCommand {
|
|
40
|
+
workspaceId: string;
|
|
41
|
+
key: string;
|
|
42
|
+
value: string;
|
|
43
|
+
scope: StoreScope;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type PutSecretCommand = PutStoreCommand;
|
|
47
|
+
export type PutVariableCommand = PutStoreCommand;
|
|
48
|
+
|
|
49
|
+
export interface DeleteStoreCommand {
|
|
50
|
+
workspaceId: string;
|
|
51
|
+
key: string;
|
|
52
|
+
scope: StoreScope;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function normalizeStoreKey(key: string): string {
|
|
56
|
+
return key.toUpperCase();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validateStoreKey(key: string): string | undefined {
|
|
60
|
+
return key.length > 0 && key.length <= STORE_KEY_MAX_LENGTH && STORE_KEY_PATTERN.test(key)
|
|
61
|
+
? undefined
|
|
62
|
+
: STORE_KEY_HELP;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateNewStoreKey(
|
|
66
|
+
key: string,
|
|
67
|
+
params: {mode: 'create' | 'edit'; reservedKeys: readonly string[]; kind: 'secret' | 'variable'},
|
|
68
|
+
): string | undefined {
|
|
69
|
+
const formatError = validateStoreKey(key);
|
|
70
|
+
if (formatError) return formatError;
|
|
71
|
+
if (params.mode === 'create' && params.reservedKeys.includes(key)) {
|
|
72
|
+
return `A ${params.kind} with this name already exists. Edit it instead.`;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function shouldWarnShortSecretValue(value: string, threshold: number): boolean {
|
|
78
|
+
return value.length > 0 && value.length < threshold;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function shouldWarnSensitiveVariableName(key: string): boolean {
|
|
82
|
+
return SENSITIVE_NAME_PATTERNS.some((pattern) => key.includes(pattern));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function projectIdFromScope(scope: StoreScope): string | undefined {
|
|
86
|
+
return scope.kind === 'project' ? scope.projectId : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function scopeFromProjectId(projectId: string | null): StoreScope {
|
|
90
|
+
return projectId ? {kind: 'project', projectId} : workspaceStoreScope;
|
|
91
|
+
}
|
package/src/feature.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import {defineClientFeature} from '@shipfox/client-shell';
|
|
1
|
+
import {defineClientFeature, type SettingsSectionEntry} from '@shipfox/client-shell';
|
|
2
|
+
|
|
3
|
+
export const secretsSettingsSections = [
|
|
4
|
+
{id: 'settings.secrets', pathSegment: 'secrets', label: 'Secrets', icon: 'keyLine', order: 500},
|
|
5
|
+
{
|
|
6
|
+
id: 'settings.variables',
|
|
7
|
+
pathSegment: 'variables',
|
|
8
|
+
label: 'Variables',
|
|
9
|
+
icon: 'bracesLine',
|
|
10
|
+
order: 600,
|
|
11
|
+
},
|
|
12
|
+
] as const satisfies readonly SettingsSectionEntry[];
|
|
2
13
|
|
|
3
14
|
export const secretsFeature = defineClientFeature({
|
|
4
15
|
id: 'shipfox.secrets',
|
|
@@ -14,14 +25,5 @@ export const secretsFeature = defineClientFeature({
|
|
|
14
25
|
impl: '@shipfox/client-secrets/routes/variables-settings',
|
|
15
26
|
},
|
|
16
27
|
],
|
|
17
|
-
settingsSections:
|
|
18
|
-
{id: 'settings.secrets', pathSegment: 'secrets', label: 'Secrets', icon: 'keyLine', order: 500},
|
|
19
|
-
{
|
|
20
|
-
id: 'settings.variables',
|
|
21
|
-
pathSegment: 'variables',
|
|
22
|
-
label: 'Variables',
|
|
23
|
-
icon: 'bracesLine',
|
|
24
|
-
order: 600,
|
|
25
|
-
},
|
|
26
|
-
],
|
|
28
|
+
settingsSections: secretsSettingsSections,
|
|
27
29
|
});
|