@cat-factory/app 0.18.1 → 0.19.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.
- package/app/components/layout/IntegrationsHub.vue +19 -1
- package/app/components/settings/UserSecretsSection.vue +214 -0
- package/app/composables/api/userSecrets.ts +31 -0
- package/app/composables/useApi.ts +2 -0
- package/app/pages/index.vue +2 -0
- package/app/stores/ui.ts +10 -0
- package/app/stores/userSecrets.ts +64 -0
- package/app/types/userSecrets.ts +49 -0
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ const documents = useDocumentsStore()
|
|
|
14
14
|
const tasks = useTasksStore()
|
|
15
15
|
const tracker = useTrackerStore()
|
|
16
16
|
const releaseHealth = useReleaseHealthStore()
|
|
17
|
+
const userSecrets = useUserSecretsStore()
|
|
17
18
|
|
|
18
19
|
// The selected filing tracker, as a badge label ("GitHub Issues" / "Jira").
|
|
19
20
|
const trackerLabel = computed(() => {
|
|
@@ -27,7 +28,10 @@ const trackerLabel = computed(() => {
|
|
|
27
28
|
watch(
|
|
28
29
|
() => ui.integrationsOpen,
|
|
29
30
|
(isOpen) => {
|
|
30
|
-
if (isOpen)
|
|
31
|
+
if (isOpen) {
|
|
32
|
+
void releaseHealth.ensureLoaded().catch(() => {})
|
|
33
|
+
void userSecrets.load().catch(() => {})
|
|
34
|
+
}
|
|
31
35
|
},
|
|
32
36
|
)
|
|
33
37
|
|
|
@@ -75,6 +79,20 @@ const groups = computed<IntegrationGroup[]>(() => {
|
|
|
75
79
|
onClick: () => go(ui.openGitHub),
|
|
76
80
|
})
|
|
77
81
|
}
|
|
82
|
+
// Per-user GitHub PAT — works on every runtime (used for runs you initiate). Always
|
|
83
|
+
// offered; the badge reflects whether the signed-in user has stored one.
|
|
84
|
+
{
|
|
85
|
+
const pat = userSecrets.statusFor('github_pat')
|
|
86
|
+
code.push({
|
|
87
|
+
key: 'github-pat',
|
|
88
|
+
icon: 'i-lucide-key-round',
|
|
89
|
+
label: 'My GitHub token',
|
|
90
|
+
description: 'A personal access token used for runs you start (pushes, PRs, CI, merge).',
|
|
91
|
+
status: pat ? 'Connected' : undefined,
|
|
92
|
+
connected: !!pat,
|
|
93
|
+
onClick: () => go(ui.openUserSecrets),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
78
96
|
if (code.length) out.push({ title: 'Source control', items: code })
|
|
79
97
|
|
|
80
98
|
// --- Communication ---------------------------------------------------------
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Per-user settings: "My GitHub token" (and future per-user repository/provider secrets).
|
|
3
|
+
// A generic, descriptor-driven connect form: the backend declares each kind's fields
|
|
4
|
+
// (one secret + optional metadata) and whether a connection test is available; this
|
|
5
|
+
// renders them without hard-coding any kind. Stored PER USER (runs you initiate use YOUR
|
|
6
|
+
// access); the secret is write-only server-side and never shown again.
|
|
7
|
+
import { computed, ref, watch } from 'vue'
|
|
8
|
+
import type { ProviderConfigField, UserSecretKind } from '~/types/userSecrets'
|
|
9
|
+
|
|
10
|
+
const ui = useUiStore()
|
|
11
|
+
const store = useUserSecretsStore()
|
|
12
|
+
const toast = useToast()
|
|
13
|
+
|
|
14
|
+
const open = computed({
|
|
15
|
+
get: () => ui.userSecretsOpen,
|
|
16
|
+
set: (v: boolean) => (v ? ui.openUserSecrets() : ui.closeUserSecrets()),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// The kind being edited (only `github_pat` today; descriptors drive the rest generically).
|
|
20
|
+
const kind = ref<UserSecretKind>('github_pat')
|
|
21
|
+
const descriptor = computed(() => store.descriptorFor(kind.value))
|
|
22
|
+
const status = computed(() => store.statusFor(kind.value))
|
|
23
|
+
|
|
24
|
+
// Per-field draft values, keyed by field key. The secret field maps to the wire `secret`;
|
|
25
|
+
// all other fields map into `metadata`.
|
|
26
|
+
const values = ref<Record<string, string>>({})
|
|
27
|
+
const labelDraft = ref('')
|
|
28
|
+
const testResult = ref<{ ok: boolean; message?: string } | null>(null)
|
|
29
|
+
const testing = ref(false)
|
|
30
|
+
const busy = ref(false)
|
|
31
|
+
|
|
32
|
+
function resetDraft() {
|
|
33
|
+
values.value = {}
|
|
34
|
+
labelDraft.value = ''
|
|
35
|
+
testResult.value = null
|
|
36
|
+
// Prefill non-secret metadata from the stored status (secret stays blank — write-only).
|
|
37
|
+
const meta = status.value?.metadata
|
|
38
|
+
if (meta) for (const [k, v] of Object.entries(meta)) values.value[k] = v
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
watch(open, (isOpen) => {
|
|
42
|
+
if (isOpen) void store.load().then(resetDraft)
|
|
43
|
+
})
|
|
44
|
+
watch(kind, resetDraft)
|
|
45
|
+
|
|
46
|
+
const secretField = computed<ProviderConfigField | undefined>(() =>
|
|
47
|
+
descriptor.value?.configFields.find((f) => f.secret),
|
|
48
|
+
)
|
|
49
|
+
const metadataFields = computed<ProviderConfigField[]>(() =>
|
|
50
|
+
(descriptor.value?.configFields ?? []).filter((f) => !f.secret),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
/** Build the wire payload: the secret field → `secret`, the rest → `metadata`. */
|
|
54
|
+
function buildPayload(): { secret: string; metadata?: Record<string, string> } | null {
|
|
55
|
+
const sf = secretField.value
|
|
56
|
+
if (!sf) return null
|
|
57
|
+
const secret = (values.value[sf.key] ?? '').trim()
|
|
58
|
+
if (!secret) return null
|
|
59
|
+
const metadata: Record<string, string> = {}
|
|
60
|
+
for (const f of metadataFields.value) {
|
|
61
|
+
const v = (values.value[f.key] ?? '').trim()
|
|
62
|
+
if (v) metadata[f.key] = v
|
|
63
|
+
}
|
|
64
|
+
return { secret, ...(Object.keys(metadata).length ? { metadata } : {}) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function notifyError(title: string, e: unknown) {
|
|
68
|
+
toast.add({
|
|
69
|
+
title,
|
|
70
|
+
description: e instanceof Error ? e.message : String(e),
|
|
71
|
+
icon: 'i-lucide-triangle-alert',
|
|
72
|
+
color: 'error',
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function test() {
|
|
77
|
+
const payload = buildPayload()
|
|
78
|
+
if (!payload) return
|
|
79
|
+
testing.value = true
|
|
80
|
+
testResult.value = null
|
|
81
|
+
try {
|
|
82
|
+
testResult.value = await store.test(kind.value, payload)
|
|
83
|
+
} catch (e) {
|
|
84
|
+
testResult.value = { ok: false, message: e instanceof Error ? e.message : String(e) }
|
|
85
|
+
} finally {
|
|
86
|
+
testing.value = false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function save() {
|
|
91
|
+
const payload = buildPayload()
|
|
92
|
+
if (!payload) return
|
|
93
|
+
busy.value = true
|
|
94
|
+
try {
|
|
95
|
+
await store.store(kind.value, { ...payload, label: labelDraft.value.trim() || undefined })
|
|
96
|
+
values.value[secretField.value!.key] = ''
|
|
97
|
+
testResult.value = null
|
|
98
|
+
toast.add({
|
|
99
|
+
title: `${descriptor.value?.label ?? 'Secret'} saved`,
|
|
100
|
+
icon: 'i-lucide-check',
|
|
101
|
+
color: 'success',
|
|
102
|
+
})
|
|
103
|
+
} catch (e) {
|
|
104
|
+
notifyError('Could not save secret', e)
|
|
105
|
+
} finally {
|
|
106
|
+
busy.value = false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function remove() {
|
|
111
|
+
busy.value = true
|
|
112
|
+
try {
|
|
113
|
+
await store.remove(kind.value)
|
|
114
|
+
resetDraft()
|
|
115
|
+
toast.add({ title: 'Secret removed', icon: 'i-lucide-check' })
|
|
116
|
+
} catch (e) {
|
|
117
|
+
notifyError('Could not remove secret', e)
|
|
118
|
+
} finally {
|
|
119
|
+
busy.value = false
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<template>
|
|
125
|
+
<UModal v-model:open="open" title="My GitHub token" :ui="{ content: 'max-w-xl' }">
|
|
126
|
+
<template #body>
|
|
127
|
+
<div class="space-y-4">
|
|
128
|
+
<p class="text-xs text-slate-400">
|
|
129
|
+
Store a personal access token used for the runs <strong>you</strong> start — pushes, pull
|
|
130
|
+
requests, the CI gate and merges are attributed to your GitHub access. Stored
|
|
131
|
+
<span class="text-slate-300">just for you</span>; the token is write-only and never shown
|
|
132
|
+
again.
|
|
133
|
+
</p>
|
|
134
|
+
|
|
135
|
+
<div
|
|
136
|
+
v-if="status"
|
|
137
|
+
class="flex items-center justify-between rounded-md border border-slate-700 bg-slate-900/50 px-3 py-2 text-sm"
|
|
138
|
+
>
|
|
139
|
+
<div>
|
|
140
|
+
<span class="font-medium text-slate-200">{{ status.label }}</span>
|
|
141
|
+
<div class="text-[11px] text-emerald-400">Connected · token stored</div>
|
|
142
|
+
</div>
|
|
143
|
+
<UButton
|
|
144
|
+
icon="i-lucide-trash-2"
|
|
145
|
+
color="error"
|
|
146
|
+
variant="ghost"
|
|
147
|
+
size="xs"
|
|
148
|
+
:disabled="busy"
|
|
149
|
+
@click="remove()"
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div
|
|
154
|
+
v-if="descriptor"
|
|
155
|
+
class="rounded-lg border border-dashed border-slate-700 p-3 space-y-3"
|
|
156
|
+
>
|
|
157
|
+
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
|
158
|
+
{{ status ? 'Replace token' : 'Add a token' }}
|
|
159
|
+
</p>
|
|
160
|
+
|
|
161
|
+
<UFormField label="Label (optional)">
|
|
162
|
+
<UInput v-model="labelDraft" :placeholder="descriptor.label" />
|
|
163
|
+
</UFormField>
|
|
164
|
+
|
|
165
|
+
<UFormField
|
|
166
|
+
v-for="field in descriptor.configFields"
|
|
167
|
+
:key="field.key"
|
|
168
|
+
:label="field.label + (field.required ? '' : ' (optional)')"
|
|
169
|
+
:help="field.help"
|
|
170
|
+
>
|
|
171
|
+
<UInput
|
|
172
|
+
v-model="values[field.key]"
|
|
173
|
+
:type="field.secret ? 'password' : 'text'"
|
|
174
|
+
class="font-mono"
|
|
175
|
+
:placeholder="field.placeholder"
|
|
176
|
+
/>
|
|
177
|
+
</UFormField>
|
|
178
|
+
|
|
179
|
+
<div v-if="descriptor.supportsTest" class="flex items-center gap-2">
|
|
180
|
+
<UButton
|
|
181
|
+
color="neutral"
|
|
182
|
+
variant="soft"
|
|
183
|
+
size="sm"
|
|
184
|
+
icon="i-lucide-plug-zap"
|
|
185
|
+
:loading="testing"
|
|
186
|
+
:disabled="!buildPayload()"
|
|
187
|
+
@click="test()"
|
|
188
|
+
>
|
|
189
|
+
Test connection
|
|
190
|
+
</UButton>
|
|
191
|
+
<span v-if="testResult && testResult.ok" class="text-xs text-emerald-400">
|
|
192
|
+
{{ testResult.message ?? 'Token valid' }}
|
|
193
|
+
</span>
|
|
194
|
+
<span v-else-if="testResult" class="text-xs text-rose-400">
|
|
195
|
+
{{ testResult.message ?? 'Token rejected' }}
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div class="flex justify-end">
|
|
200
|
+
<UButton
|
|
201
|
+
color="primary"
|
|
202
|
+
size="sm"
|
|
203
|
+
:loading="busy"
|
|
204
|
+
:disabled="!buildPayload()"
|
|
205
|
+
@click="save()"
|
|
206
|
+
>
|
|
207
|
+
{{ status ? 'Save' : 'Add token' }}
|
|
208
|
+
</UButton>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</template>
|
|
213
|
+
</UModal>
|
|
214
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConnectionTestResult,
|
|
3
|
+
StoreUserSecretInput,
|
|
4
|
+
TestUserSecretInput,
|
|
5
|
+
UserSecretDescriptor,
|
|
6
|
+
UserSecretKind,
|
|
7
|
+
UserSecretStatus,
|
|
8
|
+
} from '~/types/userSecrets'
|
|
9
|
+
import type { ApiContext } from './context'
|
|
10
|
+
|
|
11
|
+
// Per-USER generic secrets (a GitHub PAT today). User-scoped (no workspace); the secret
|
|
12
|
+
// is write-only server-side and never returned — only status metadata + a `hasSecret`
|
|
13
|
+
// flag. `descriptors` drive the generic connect form; `test` probes before save.
|
|
14
|
+
export function userSecretsApi({ http }: ApiContext) {
|
|
15
|
+
return {
|
|
16
|
+
listUserSecrets: () =>
|
|
17
|
+
http<{ secrets: UserSecretStatus[]; descriptors: UserSecretDescriptor[] }>('/user-secrets'),
|
|
18
|
+
|
|
19
|
+
storeUserSecret: (kind: UserSecretKind, body: StoreUserSecretInput) =>
|
|
20
|
+
http<UserSecretStatus>(`/user-secrets/${encodeURIComponent(kind)}`, { method: 'POST', body }),
|
|
21
|
+
|
|
22
|
+
deleteUserSecret: (kind: UserSecretKind) =>
|
|
23
|
+
http(`/user-secrets/${encodeURIComponent(kind)}`, { method: 'DELETE' }),
|
|
24
|
+
|
|
25
|
+
testUserSecret: (kind: UserSecretKind, body: TestUserSecretInput) =>
|
|
26
|
+
http<ConnectionTestResult>(`/user-secrets/${encodeURIComponent(kind)}/test`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
body,
|
|
29
|
+
}),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -17,6 +17,7 @@ import { reviewsApi } from './api/reviews'
|
|
|
17
17
|
import { slackApi } from './api/slack'
|
|
18
18
|
import { specApi } from './api/spec'
|
|
19
19
|
import { tasksApi } from './api/tasks'
|
|
20
|
+
import { userSecretsApi } from './api/userSecrets'
|
|
20
21
|
import { workspacesApi } from './api/workspaces'
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -87,5 +88,6 @@ export function useApi() {
|
|
|
87
88
|
...githubApi(ctx),
|
|
88
89
|
...slackApi(ctx),
|
|
89
90
|
...bootstrapApi(ctx),
|
|
91
|
+
...userSecretsApi(ctx),
|
|
90
92
|
}
|
|
91
93
|
}
|
package/app/pages/index.vue
CHANGED
|
@@ -31,6 +31,7 @@ import WorkspaceSettingsPanel from '~/components/settings/WorkspaceSettingsPanel
|
|
|
31
31
|
import ObservabilityConnectionPanel from '~/components/settings/ObservabilityConnectionPanel.vue'
|
|
32
32
|
import ModelConfigurationPanel from '~/components/settings/ModelConfigurationPanel.vue'
|
|
33
33
|
import LocalModelEndpointsPanel from '~/components/settings/LocalModelEndpointsPanel.vue'
|
|
34
|
+
import UserSecretsSection from '~/components/settings/UserSecretsSection.vue'
|
|
34
35
|
import OpenRouterCatalogPanel from '~/components/settings/OpenRouterCatalogPanel.vue'
|
|
35
36
|
import VendorCredentialsModal from '~/components/providers/VendorCredentialsModal.vue'
|
|
36
37
|
import PersonalCredentialModal from '~/components/providers/PersonalCredentialModal.vue'
|
|
@@ -183,6 +184,7 @@ watch(
|
|
|
183
184
|
<ObservabilityConnectionPanel />
|
|
184
185
|
<ModelConfigurationPanel />
|
|
185
186
|
<LocalModelEndpointsPanel />
|
|
187
|
+
<UserSecretsSection />
|
|
186
188
|
<OpenRouterCatalogPanel />
|
|
187
189
|
<VendorCredentialsModal />
|
|
188
190
|
<PersonalCredentialModal />
|
package/app/stores/ui.ts
CHANGED
|
@@ -102,6 +102,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
102
102
|
const vendorCredentialsOpen = ref(false)
|
|
103
103
|
// Per-user settings panel: the signed-in user's own-machine local model runners.
|
|
104
104
|
const localModelsOpen = ref(false)
|
|
105
|
+
const userSecretsOpen = ref(false)
|
|
105
106
|
// Per-workspace settings panel: the OpenRouter dynamic catalog (browse/enable gateway models).
|
|
106
107
|
const openRouterOpen = ref(false)
|
|
107
108
|
|
|
@@ -358,6 +359,12 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
358
359
|
function closeLocalModels() {
|
|
359
360
|
localModelsOpen.value = false
|
|
360
361
|
}
|
|
362
|
+
function openUserSecrets() {
|
|
363
|
+
userSecretsOpen.value = true
|
|
364
|
+
}
|
|
365
|
+
function closeUserSecrets() {
|
|
366
|
+
userSecretsOpen.value = false
|
|
367
|
+
}
|
|
361
368
|
function openOpenRouter() {
|
|
362
369
|
openRouterOpen.value = true
|
|
363
370
|
}
|
|
@@ -451,6 +458,7 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
451
458
|
modelConfigOpen,
|
|
452
459
|
vendorCredentialsOpen,
|
|
453
460
|
localModelsOpen,
|
|
461
|
+
userSecretsOpen,
|
|
454
462
|
openRouterOpen,
|
|
455
463
|
aiProviderSetupOpen,
|
|
456
464
|
aiPresetMismatchOpen,
|
|
@@ -512,6 +520,8 @@ export const useUiStore = defineStore('ui', () => {
|
|
|
512
520
|
closeVendorCredentials,
|
|
513
521
|
openLocalModels,
|
|
514
522
|
closeLocalModels,
|
|
523
|
+
openUserSecrets,
|
|
524
|
+
closeUserSecrets,
|
|
515
525
|
openOpenRouter,
|
|
516
526
|
closeOpenRouter,
|
|
517
527
|
openAiProviderSetup,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
ConnectionTestResult,
|
|
5
|
+
StoreUserSecretInput,
|
|
6
|
+
TestUserSecretInput,
|
|
7
|
+
UserSecretDescriptor,
|
|
8
|
+
UserSecretKind,
|
|
9
|
+
UserSecretStatus,
|
|
10
|
+
} from '~/types/userSecrets'
|
|
11
|
+
|
|
12
|
+
// The signed-in user's generic secrets (a GitHub PAT today). Stored PER USER (a run you
|
|
13
|
+
// initiate uses YOUR GitHub access), write-only server-side — this store carries only the
|
|
14
|
+
// status metadata + a `hasSecret` flag, plus the kind descriptors that drive the generic
|
|
15
|
+
// connect form. Loaded INDEPENDENTLY of the workspace snapshot, like local runners.
|
|
16
|
+
export const useUserSecretsStore = defineStore('userSecrets', () => {
|
|
17
|
+
const api = useApi()
|
|
18
|
+
const secrets = ref<UserSecretStatus[]>([])
|
|
19
|
+
const descriptors = ref<UserSecretDescriptor[]>([])
|
|
20
|
+
const loading = ref(false)
|
|
21
|
+
|
|
22
|
+
async function load() {
|
|
23
|
+
loading.value = true
|
|
24
|
+
try {
|
|
25
|
+
const { secrets: list, descriptors: descs } = await api.listUserSecrets()
|
|
26
|
+
secrets.value = list
|
|
27
|
+
descriptors.value = descs
|
|
28
|
+
} catch {
|
|
29
|
+
// Auth disabled / not signed in / feature off → nothing to surface.
|
|
30
|
+
secrets.value = []
|
|
31
|
+
descriptors.value = []
|
|
32
|
+
} finally {
|
|
33
|
+
loading.value = false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function statusFor(kind: UserSecretKind): UserSecretStatus | undefined {
|
|
38
|
+
return secrets.value.find((s) => s.kind === kind)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function descriptorFor(kind: UserSecretKind): UserSecretDescriptor | undefined {
|
|
42
|
+
return descriptors.value.find((d) => d.kind === kind)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function store(kind: UserSecretKind, input: StoreUserSecretInput) {
|
|
46
|
+
const status = await api.storeUserSecret(kind, input)
|
|
47
|
+
secrets.value = [...secrets.value.filter((s) => s.kind !== kind), status]
|
|
48
|
+
return status
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function remove(kind: UserSecretKind) {
|
|
52
|
+
await api.deleteUserSecret(kind)
|
|
53
|
+
secrets.value = secrets.value.filter((s) => s.kind !== kind)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function test(
|
|
57
|
+
kind: UserSecretKind,
|
|
58
|
+
input: TestUserSecretInput,
|
|
59
|
+
): Promise<ConnectionTestResult> {
|
|
60
|
+
return await api.testUserSecret(kind, input)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { secrets, descriptors, loading, load, statusFor, descriptorFor, store, remove, test }
|
|
64
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Frontend mirrors of the per-user secret + provider-config wire contracts
|
|
2
|
+
// (`@cat-factory/contracts` user-secret.ts + provider-config.ts).
|
|
3
|
+
|
|
4
|
+
export type UserSecretKind = 'github_pat'
|
|
5
|
+
|
|
6
|
+
/** One config value a kind needs, rendered as a single form field. */
|
|
7
|
+
export interface ProviderConfigField {
|
|
8
|
+
key: string
|
|
9
|
+
label: string
|
|
10
|
+
help?: string
|
|
11
|
+
placeholder?: string
|
|
12
|
+
secret?: boolean
|
|
13
|
+
required?: boolean
|
|
14
|
+
type?: 'text' | 'password' | 'select'
|
|
15
|
+
options?: { value: string; label: string }[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Read-only status of one stored per-user secret — never the secret value. */
|
|
19
|
+
export interface UserSecretStatus {
|
|
20
|
+
kind: UserSecretKind
|
|
21
|
+
label: string
|
|
22
|
+
hasSecret: boolean
|
|
23
|
+
metadata?: Record<string, string>
|
|
24
|
+
connectedAt: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A kind's self-description for the generic connect form. */
|
|
28
|
+
export interface UserSecretDescriptor {
|
|
29
|
+
kind: UserSecretKind
|
|
30
|
+
label: string
|
|
31
|
+
configFields: ProviderConfigField[]
|
|
32
|
+
supportsTest: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StoreUserSecretInput {
|
|
36
|
+
label?: string
|
|
37
|
+
secret: string
|
|
38
|
+
metadata?: Record<string, string>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TestUserSecretInput {
|
|
42
|
+
secret: string
|
|
43
|
+
metadata?: Record<string, string>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ConnectionTestResult {
|
|
47
|
+
ok: boolean
|
|
48
|
+
message?: string
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|