@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.
Files changed (72) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +38 -0
  3. package/dist/components/form-errors.d.ts.map +1 -1
  4. package/dist/components/form-errors.js +0 -6
  5. package/dist/components/form-errors.js.map +1 -1
  6. package/dist/components/secret-form.d.ts.map +1 -1
  7. package/dist/components/secret-form.js +6 -8
  8. package/dist/components/secret-form.js.map +1 -1
  9. package/dist/components/variable-form.d.ts.map +1 -1
  10. package/dist/components/variable-form.js +6 -8
  11. package/dist/components/variable-form.js.map +1 -1
  12. package/dist/components/workspace-secrets-section.d.ts.map +1 -1
  13. package/dist/components/workspace-secrets-section.js +4 -2
  14. package/dist/components/workspace-secrets-section.js.map +1 -1
  15. package/dist/components/workspace-secrets-section.stories.d.ts.map +1 -1
  16. package/dist/components/workspace-secrets-section.stories.js.map +1 -1
  17. package/dist/components/workspace-variables-section.d.ts.map +1 -1
  18. package/dist/components/workspace-variables-section.js +5 -3
  19. package/dist/components/workspace-variables-section.js.map +1 -1
  20. package/dist/components/workspace-variables-section.stories.d.ts.map +1 -1
  21. package/dist/components/workspace-variables-section.stories.js.map +1 -1
  22. package/dist/core/store.d.ts +54 -0
  23. package/dist/core/store.d.ts.map +1 -0
  24. package/dist/core/store.js +44 -0
  25. package/dist/core/store.js.map +1 -0
  26. package/dist/feature.d.ts +13 -0
  27. package/dist/feature.d.ts.map +1 -1
  28. package/dist/feature.js +17 -16
  29. package/dist/feature.js.map +1 -1
  30. package/dist/hooks/api/create-store-api.d.ts +31 -49
  31. package/dist/hooks/api/create-store-api.d.ts.map +1 -1
  32. package/dist/hooks/api/create-store-api.js +44 -45
  33. package/dist/hooks/api/create-store-api.js.map +1 -1
  34. package/dist/hooks/api/secrets.d.ts +16 -55
  35. package/dist/hooks/api/secrets.d.ts.map +1 -1
  36. package/dist/hooks/api/secrets.js +10 -2
  37. package/dist/hooks/api/secrets.js.map +1 -1
  38. package/dist/hooks/api/store-mapper.d.ts +7 -0
  39. package/dist/hooks/api/store-mapper.d.ts.map +1 -0
  40. package/dist/hooks/api/store-mapper.js +36 -0
  41. package/dist/hooks/api/store-mapper.js.map +1 -0
  42. package/dist/hooks/api/variables.d.ts +25 -78
  43. package/dist/hooks/api/variables.d.ts.map +1 -1
  44. package/dist/hooks/api/variables.js +37 -28
  45. package/dist/hooks/api/variables.js.map +1 -1
  46. package/dist/tsconfig.test.tsbuildinfo +1 -1
  47. package/package.json +9 -10
  48. package/src/components/form-errors.test.ts +2 -2
  49. package/src/components/form-errors.ts +0 -3
  50. package/src/components/secret-form.tsx +15 -5
  51. package/src/components/variable-form.tsx +15 -5
  52. package/src/components/workspace-secrets-section.stories.tsx +1 -2
  53. package/src/components/workspace-secrets-section.tsx +8 -4
  54. package/src/components/workspace-variables-section.stories.tsx +1 -2
  55. package/src/components/workspace-variables-section.tsx +10 -6
  56. package/src/core/store.test.ts +38 -0
  57. package/src/core/store.ts +91 -0
  58. package/src/feature.ts +13 -11
  59. package/src/hooks/api/create-store-api.ts +104 -75
  60. package/src/hooks/api/secrets.test.ts +16 -10
  61. package/src/hooks/api/secrets.ts +11 -14
  62. package/src/hooks/api/store-mapper.test.ts +39 -0
  63. package/src/hooks/api/store-mapper.ts +47 -0
  64. package/src/hooks/api/variables.ts +52 -36
  65. package/tsconfig.build.tsbuildinfo +1 -1
  66. package/vercel.json +5 -0
  67. package/dist/components/store-key.d.ts +0 -20
  68. package/dist/components/store-key.d.ts.map +0 -1
  69. package/dist/components/store-key.js +0 -25
  70. package/dist/components/store-key.js.map +0 -1
  71. package/src/components/store-key.test.ts +0 -53
  72. 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": "5.0.0",
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": "6.0.0",
35
- "@shipfox/client-api": "4.0.0",
36
- "@shipfox/client-shell": "5.0.0",
37
- "@shipfox/client-ui": "4.0.0",
38
- "@shipfox/react-ui": "0.3.5"
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('maps a plain Error to a form-level alert with its message', () => {
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: 'boom'});
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({workspaceId, key: value.key, body: {value: value.value}});
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.toUpperCase())}
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
- {isShortSecretValue(field.state.value, SHORT_VALUE_THRESHOLD) ? (
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({workspaceId, key: value.key, body: {value: value.value}});
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.toUpperCase())}
128
+ onChange={(event) => field.handleChange(normalizeStoreKey(event.target.value))}
119
129
  onBlur={field.handleBlur}
120
130
  />
121
- {isSensitiveSecretName(field.state.value) ? (
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: SecretDto[] = [
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({workspaceId, key: deleteKey});
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: SecretDto;
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.updated_at} />
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: VariableListItemDto[] = [
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: VariableListItemDto} | null;
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.value_truncated : undefined
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({workspaceId, key: deleteKey});
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: VariableListItemDto;
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.updated_at} />
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
  });