@cat-factory/app 0.125.0 → 0.126.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.
@@ -5,11 +5,30 @@
5
5
  // only (label + created / last-used). To rotate a token, revoke it and mint a new one.
6
6
  // Opened from the Integrations hub.
7
7
  import { computed, ref, watch } from 'vue'
8
- import type { PublicApiKey } from '~/types/publicApiKeys'
8
+ import type { PublicApiKey, PublicApiScope } from '~/types/publicApiKeys'
9
9
  import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
10
10
  import CopyButton from '~/components/common/CopyButton.vue'
11
11
 
12
12
  const { t, d } = useI18n()
13
+
14
+ // The permission ladder a minted key can carry (read ⊂ write ⊂ admin), mirroring the backend
15
+ // contract. A `read` key can only observe; `write` adds create/start/manage; `admin` adds the
16
+ // destructive/merge-adjacent operations (e.g. deleting a task).
17
+ const SCOPES: PublicApiScope[] = ['read', 'write', 'admin']
18
+
19
+ /** Localized label for a scope — an exhaustive switch, so a new scope is a compile error here. */
20
+ function scopeLabel(scope: PublicApiScope): string {
21
+ switch (scope) {
22
+ case 'read':
23
+ return t('settings.apiTokens.scopes.read')
24
+ case 'write':
25
+ return t('settings.apiTokens.scopes.write')
26
+ case 'admin':
27
+ return t('settings.apiTokens.scopes.admin')
28
+ }
29
+ }
30
+
31
+ const scopeItems = computed(() => SCOPES.map((value) => ({ value, label: scopeLabel(value) })))
13
32
  const ui = useUiStore()
14
33
  const store = usePublicApiKeysStore()
15
34
  const toast = useToast()
@@ -22,6 +41,8 @@ const open = computed({
22
41
  const back = useIntegrationBack(open)
23
42
 
24
43
  const label = ref('')
44
+ // The scope the next minted key will carry; defaults to the safe middle of the ladder.
45
+ const scope = ref<PublicApiScope>('write')
25
46
  const busy = ref(false)
26
47
  // The full raw secret from the most recent create — surfaced once, then dismissed. Never
27
48
  // re-fetchable, so it lives only in this transient ref (not the store).
@@ -58,9 +79,10 @@ async function createToken() {
58
79
  if (!trimmed) return
59
80
  busy.value = true
60
81
  try {
61
- const created = await store.create(trimmed)
82
+ const created = await store.create(trimmed, scope.value)
62
83
  newSecret.value = created.secret
63
84
  label.value = ''
85
+ scope.value = 'write'
64
86
  toast.add({
65
87
  title: t('settings.apiTokens.toast.created'),
66
88
  icon: 'i-lucide-check',
@@ -145,7 +167,17 @@ async function revokeToken(key: PublicApiKey) {
145
167
  class="flex items-center justify-between gap-2 rounded-md border border-slate-800 px-3 py-2"
146
168
  >
147
169
  <div class="min-w-0 space-y-0.5">
148
- <div class="truncate text-sm font-medium">{{ key.label }}</div>
170
+ <div class="flex items-center gap-2">
171
+ <span class="truncate text-sm font-medium">{{ key.label }}</span>
172
+ <UBadge
173
+ color="neutral"
174
+ variant="subtle"
175
+ size="sm"
176
+ :data-testid="`api-token-scope-${key.id}`"
177
+ >
178
+ {{ scopeLabel(key.scope) }}
179
+ </UBadge>
180
+ </div>
149
181
  <div class="text-[11px] text-slate-500">
150
182
  {{
151
183
  t('settings.apiTokens.list.created', {
@@ -190,6 +222,17 @@ async function revokeToken(key: PublicApiKey) {
190
222
  @keyup.enter="createToken"
191
223
  />
192
224
  </UFormField>
225
+ <UFormField
226
+ :label="t('settings.apiTokens.add.scope')"
227
+ :help="t('settings.apiTokens.add.scopeHelp')"
228
+ >
229
+ <USelect
230
+ v-model="scope"
231
+ :items="scopeItems"
232
+ class="w-full"
233
+ data-testid="api-token-scope"
234
+ />
235
+ </UFormField>
193
236
  <UButton
194
237
  :loading="busy"
195
238
  :disabled="!label.trim()"
@@ -10,6 +10,7 @@ function key(over: Partial<PublicApiKey> = {}): PublicApiKey {
10
10
  accountId: 'acc1',
11
11
  workspaceId: 'ws1',
12
12
  label: 'CI',
13
+ scope: 'write',
13
14
  createdAt: 1,
14
15
  lastUsedAt: null,
15
16
  revokedAt: null,
@@ -83,7 +84,7 @@ describe('publicApiKeys store', () => {
83
84
 
84
85
  const store = usePublicApiKeysStore()
85
86
  await store.load()
86
- const result = await store.create('deploy')
87
+ const result = await store.create('deploy', 'admin')
87
88
 
88
89
  expect(result.secret).toBe('cf_live_pak_new.abc')
89
90
  expect(store.keys.map((k) => k.id)).toEqual(['pak_new', 'pak_old'])
@@ -1,6 +1,6 @@
1
1
  import { defineStore } from 'pinia'
2
2
  import { ref } from 'vue'
3
- import type { CreatedPublicApiKey, PublicApiKey } from '~/types/publicApiKeys'
3
+ import type { CreatedPublicApiKey, PublicApiKey, PublicApiScope } from '~/types/publicApiKeys'
4
4
  import { useWorkspaceStore } from '~/stores/workspace'
5
5
  import { apiErrorStatus } from '~/composables/api/errors'
6
6
 
@@ -51,10 +51,13 @@ export const usePublicApiKeysStore = defineStore('publicApiKeys', () => {
51
51
  return inFlight
52
52
  }
53
53
 
54
- /** Mint a key. Returns the created record PLUS the one-time raw secret (shown once). */
55
- async function create(label: string): Promise<CreatedPublicApiKey> {
54
+ /**
55
+ * Mint a key with a permission `scope` (read ⊂ write ⊂ admin). Returns the created record
56
+ * PLUS the one-time raw secret (shown once).
57
+ */
58
+ async function create(label: string, scope: PublicApiScope): Promise<CreatedPublicApiKey> {
56
59
  const ws = useWorkspaceStore()
57
- const created = await api.createPublicApiKey(ws.requireId(), { label })
60
+ const created = await api.createPublicApiKey(ws.requireId(), { label, scope })
58
61
  // Prepend: the backend lists newest-first, so the freshly minted key belongs at the
59
62
  // top — matching the order a subsequent `load()` would produce.
60
63
  keys.value = [created.key, ...keys.value]
@@ -4,6 +4,7 @@
4
4
  // Re-exported from the shared contracts package (the single source of truth).
5
5
  export type {
6
6
  PublicApiKey,
7
+ PublicApiScope,
7
8
  PublicApiKeyListResult,
8
9
  CreatePublicApiKeyInput,
9
10
  CreatedPublicApiKey,
@@ -541,8 +541,15 @@
541
541
  "label": "Bezeichnung",
542
542
  "labelHelp": "Ein Name, um dieses Token später wiederzuerkennen.",
543
543
  "labelPlaceholder": "z. B. CI-Pipeline",
544
+ "scope": "Berechtigung",
545
+ "scopeHelp": "Was dieses Token darf: nur Lesen, Lesen und Schreiben oder Vollzugriff (der auch das Löschen erlaubt).",
544
546
  "create": "Token erstellen"
545
547
  },
548
+ "scopes": {
549
+ "read": "Nur Lesen",
550
+ "write": "Lesen und Schreiben",
551
+ "admin": "Vollzugriff"
552
+ },
546
553
  "toast": {
547
554
  "loadFailed": "API-Tokens konnten nicht geladen werden",
548
555
  "created": "Token erstellt",
@@ -2488,8 +2488,15 @@
2488
2488
  "label": "Label",
2489
2489
  "labelHelp": "A name to recognize this token by later.",
2490
2490
  "labelPlaceholder": "e.g. CI pipeline",
2491
+ "scope": "Scope",
2492
+ "scopeHelp": "What this token can do: read-only, read and write, or full access (which also allows deleting).",
2491
2493
  "create": "Create token"
2492
2494
  },
2495
+ "scopes": {
2496
+ "read": "Read only",
2497
+ "write": "Read and write",
2498
+ "admin": "Full access"
2499
+ },
2493
2500
  "toast": {
2494
2501
  "loadFailed": "Could not load API tokens",
2495
2502
  "created": "Token created",
@@ -2303,8 +2303,15 @@
2303
2303
  "label": "Etiqueta",
2304
2304
  "labelHelp": "Un nombre para reconocer este token más adelante.",
2305
2305
  "labelPlaceholder": "p. ej. pipeline de CI",
2306
+ "scope": "Alcance",
2307
+ "scopeHelp": "Lo que puede hacer este token: solo lectura, lectura y escritura o acceso completo (que también permite eliminar).",
2306
2308
  "create": "Crear token"
2307
2309
  },
2310
+ "scopes": {
2311
+ "read": "Solo lectura",
2312
+ "write": "Lectura y escritura",
2313
+ "admin": "Acceso completo"
2314
+ },
2308
2315
  "toast": {
2309
2316
  "loadFailed": "No se pudieron cargar los tokens de la API",
2310
2317
  "created": "Token creado",
@@ -2303,8 +2303,15 @@
2303
2303
  "label": "Libellé",
2304
2304
  "labelHelp": "Un nom pour reconnaître ce jeton plus tard.",
2305
2305
  "labelPlaceholder": "ex. pipeline CI",
2306
+ "scope": "Portée",
2307
+ "scopeHelp": "Ce que ce jeton peut faire : lecture seule, lecture et écriture, ou accès complet (qui autorise aussi la suppression).",
2306
2308
  "create": "Créer le jeton"
2307
2309
  },
2310
+ "scopes": {
2311
+ "read": "Lecture seule",
2312
+ "write": "Lecture et écriture",
2313
+ "admin": "Accès complet"
2314
+ },
2308
2315
  "toast": {
2309
2316
  "loadFailed": "Impossible de charger les jetons d'API",
2310
2317
  "created": "Jeton créé",
@@ -2424,8 +2424,15 @@
2424
2424
  "label": "תווית",
2425
2425
  "labelHelp": "שם שיעזור לך לזהות את האסימון הזה בהמשך.",
2426
2426
  "labelPlaceholder": "לדוגמה, צינור CI",
2427
+ "scope": "היקף הרשאות",
2428
+ "scopeHelp": "מה האסימון הזה יכול לעשות: קריאה בלבד, קריאה וכתיבה, או גישה מלאה (שמאפשרת גם מחיקה).",
2427
2429
  "create": "צור אסימון"
2428
2430
  },
2431
+ "scopes": {
2432
+ "read": "קריאה בלבד",
2433
+ "write": "קריאה וכתיבה",
2434
+ "admin": "גישה מלאה"
2435
+ },
2429
2436
  "toast": {
2430
2437
  "loadFailed": "לא ניתן לטעון את אסימוני ה-API",
2431
2438
  "created": "האסימון נוצר",
@@ -541,8 +541,15 @@
541
541
  "label": "Etichetta",
542
542
  "labelHelp": "Un nome per riconoscere questo token in seguito.",
543
543
  "labelPlaceholder": "es. pipeline CI",
544
+ "scope": "Ambito",
545
+ "scopeHelp": "Cosa può fare questo token: sola lettura, lettura e scrittura o accesso completo (che consente anche l'eliminazione).",
544
546
  "create": "Crea token"
545
547
  },
548
+ "scopes": {
549
+ "read": "Sola lettura",
550
+ "write": "Lettura e scrittura",
551
+ "admin": "Accesso completo"
552
+ },
546
553
  "toast": {
547
554
  "loadFailed": "Impossibile caricare i token API",
548
555
  "created": "Token creato",
@@ -2425,8 +2425,15 @@
2425
2425
  "label": "ラベル",
2426
2426
  "labelHelp": "後でこのトークンを識別するための名前。",
2427
2427
  "labelPlaceholder": "例: CI パイプライン",
2428
+ "scope": "権限範囲",
2429
+ "scopeHelp": "このトークンでできること: 読み取り専用、読み取りと書き込み、またはフルアクセス(削除も可能)。",
2428
2430
  "create": "トークンを作成"
2429
2431
  },
2432
+ "scopes": {
2433
+ "read": "読み取り専用",
2434
+ "write": "読み取りと書き込み",
2435
+ "admin": "フルアクセス"
2436
+ },
2430
2437
  "toast": {
2431
2438
  "loadFailed": "APIトークンを読み込めませんでした",
2432
2439
  "created": "トークンを作成しました",
@@ -2303,8 +2303,15 @@
2303
2303
  "label": "Etykieta",
2304
2304
  "labelHelp": "Nazwa, po której później rozpoznasz ten token.",
2305
2305
  "labelPlaceholder": "np. potok CI",
2306
+ "scope": "Zakres",
2307
+ "scopeHelp": "Co może ten token: tylko odczyt, odczyt i zapis lub pełny dostęp (który pozwala także na usuwanie).",
2306
2308
  "create": "Utwórz token"
2307
2309
  },
2310
+ "scopes": {
2311
+ "read": "Tylko odczyt",
2312
+ "write": "Odczyt i zapis",
2313
+ "admin": "Pełny dostęp"
2314
+ },
2308
2315
  "toast": {
2309
2316
  "loadFailed": "Nie udało się załadować tokenów API",
2310
2317
  "created": "Token utworzony",
@@ -2425,8 +2425,15 @@
2425
2425
  "label": "Etiket",
2426
2426
  "labelHelp": "Bu belirteci daha sonra tanımak için bir ad.",
2427
2427
  "labelPlaceholder": "örn. CI hattı",
2428
+ "scope": "Kapsam",
2429
+ "scopeHelp": "Bu belirtecin yapabilecekleri: yalnızca okuma, okuma ve yazma ya da tam erişim (silmeye de izin verir).",
2428
2430
  "create": "Belirteç oluştur"
2429
2431
  },
2432
+ "scopes": {
2433
+ "read": "Yalnızca okuma",
2434
+ "write": "Okuma ve yazma",
2435
+ "admin": "Tam erişim"
2436
+ },
2430
2437
  "toast": {
2431
2438
  "loadFailed": "API belirteçleri yüklenemedi",
2432
2439
  "created": "Belirteç oluşturuldu",
@@ -2303,8 +2303,15 @@
2303
2303
  "label": "Мітка",
2304
2304
  "labelHelp": "Назва, за якою ви пізніше впізнаєте цей токен.",
2305
2305
  "labelPlaceholder": "напр. конвеєр CI",
2306
+ "scope": "Обсяг доступу",
2307
+ "scopeHelp": "Що може цей токен: лише читання, читання та запис або повний доступ (який також дозволяє видалення).",
2306
2308
  "create": "Створити токен"
2307
2309
  },
2310
+ "scopes": {
2311
+ "read": "Лише читання",
2312
+ "write": "Читання та запис",
2313
+ "admin": "Повний доступ"
2314
+ },
2308
2315
  "toast": {
2309
2316
  "loadFailed": "Не вдалося завантажити токени API",
2310
2317
  "created": "Токен створено",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.125.0",
3
+ "version": "0.126.1",
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",
@@ -34,7 +34,7 @@
34
34
  "valibot": "^1.4.2",
35
35
  "vue": "3.5.39",
36
36
  "wretch": "^3.0.9",
37
- "@cat-factory/contracts": "0.142.0"
37
+ "@cat-factory/contracts": "0.144.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",