@cat-factory/app 0.47.5 → 0.47.7

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.
@@ -1,16 +1,18 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref, watch } from 'vue'
2
+ import { computed, ref } from 'vue'
3
3
  import { apiErrorEnvelope } from '~/composables/api/errors'
4
4
 
5
5
  const auth = useAuthStore()
6
6
  const { t } = useI18n()
7
7
 
8
- // Local-mode source-control PAT login. GitHub/GitLab are brand names (kept verbatim across
9
- // locales), as are the token-settings URLs, so they're inline constants rather than catalog
10
- // keys same convention as the provider descriptors in ApiKeysSection. The actual link
11
- // prefers the server's scopes-preselected deep link (`patLogin.setupUrls`); these are the
12
- // fallback when it's absent.
8
+ // Local-mode source-control PAT login. The PAT lives server-side in env (GITHUB_PAT /
9
+ // GITLAB_PAT); the login screen only SELECTS a configured provider no token is ever typed
10
+ // into or shown in the browser. GitHub/GitLab are brand names (kept verbatim across locales),
11
+ // as are the token-settings URLs, so they're inline constants rather than catalog keys — same
12
+ // convention as the provider descriptors in ApiKeysSection. The "create a token" link prefers
13
+ // the server's scopes-preselected deep link (`patLogin.setupUrls`); these are the fallback.
13
14
  type PatProvider = 'github' | 'gitlab'
15
+ const ALL_PROVIDERS: PatProvider[] = ['github', 'gitlab']
14
16
  const PROVIDER_LABELS: Record<PatProvider, string> = { github: 'GitHub', gitlab: 'GitLab' }
15
17
  const PROVIDER_ICONS: Record<PatProvider, string> = {
16
18
  github: 'i-lucide-github',
@@ -23,44 +25,29 @@ const PROVIDER_TOKEN_URLS: Record<PatProvider, string> = {
23
25
  }
24
26
 
25
27
  const patLoginCfg = computed(() => auth.localMode?.patLogin)
28
+ // Only providers whose PAT is configured in env can sign in (the token is the operational
29
+ // credential too). A provider without one gets no button — see the no-PAT notice instead.
26
30
  const configuredProviders = computed<PatProvider[]>(
27
31
  () => (patLoginCfg.value?.configured ?? []) as PatProvider[],
28
32
  )
29
- const availableProviders = computed<PatProvider[]>(
30
- () => (patLoginCfg.value?.available ?? []) as PatProvider[],
31
- )
32
- const showLocalLogin = computed(() => availableProviders.value.length > 0)
33
+ const isLocalMode = computed(() => auth.localMode?.enabled === true)
34
+ const hasConfiguredPat = computed(() => configuredProviders.value.length > 0)
33
35
 
34
- const patProvider = ref<PatProvider>('github')
35
- const patToken = ref('')
36
36
  const patBusy = ref(false)
37
37
  const patError = ref<string | null>(null)
38
38
 
39
- // Keep the picker on an actually-available provider.
40
- watch(
41
- availableProviders,
42
- (list) => {
43
- if (list.length && !list.includes(patProvider.value)) patProvider.value = list[0]!
44
- },
45
- { immediate: true },
46
- )
47
-
48
- const patProviderItems = computed(() =>
49
- availableProviders.value.map((p) => ({ label: PROVIDER_LABELS[p], value: p })),
50
- )
51
-
52
- // Prefer the server's scopes-preselected deep link (it owns the per-provider scopes);
53
- // fall back to the plain token page if it wasn't advertised.
54
- const tokenCreateUrl = computed(
55
- () => patLoginCfg.value?.setupUrls?.[patProvider.value] ?? PROVIDER_TOKEN_URLS[patProvider.value],
56
- )
39
+ // Per-provider "create a token" link: prefer the server's scopes-preselected deep link (it
40
+ // owns the per-provider scopes), fall back to the plain token page.
41
+ function tokenCreateUrl(provider: PatProvider): string {
42
+ return patLoginCfg.value?.setupUrls?.[provider] ?? PROVIDER_TOKEN_URLS[provider]
43
+ }
57
44
 
58
- /** One-click (configured PAT) or pasted-token sign-in; reloads so the app boots signed in. */
59
- async function submitPat(provider: PatProvider, token?: string) {
45
+ /** Sign in as the account the configured env PAT belongs to; reloads so the app boots in. */
46
+ async function submitPat(provider: PatProvider) {
60
47
  patError.value = null
61
48
  patBusy.value = true
62
49
  try {
63
- await auth.patLogin(token ? { provider, token } : { provider })
50
+ await auth.patLogin({ provider })
64
51
  if (typeof window !== 'undefined') window.location.assign(window.location.pathname)
65
52
  } catch (e) {
66
53
  patError.value = apiErrorEnvelope(e)?.message ?? t('auth.localMode.failed')
@@ -154,9 +141,10 @@ const showOAuthDivider = computed(
154
141
  </p>
155
142
  </div>
156
143
 
157
- <!-- Local mode: sign in with a source-control PAT (no OAuth round-trip needed) -->
158
- <div v-if="showLocalLogin && mode !== 'forgot'" class="space-y-3">
159
- <!-- One-click: a PAT is already configured server-side -->
144
+ <!-- Local mode: sign in with the env-configured source-control PAT. The token lives
145
+ server-side (GITHUB_PAT / GITLAB_PAT); we only pick a provider here. -->
146
+ <div v-if="isLocalMode && mode !== 'forgot'" class="space-y-3">
147
+ <!-- One button per provider whose PAT is configured in env -->
160
148
  <UButton
161
149
  v-for="p in configuredProviders"
162
150
  :key="p"
@@ -167,53 +155,37 @@ const showOAuthDivider = computed(
167
155
  :loading="patBusy"
168
156
  @click="submitPat(p)"
169
157
  >
170
- {{ t('auth.localMode.continueWith', { provider: PROVIDER_LABELS[p] }) }}
158
+ {{ t('auth.localMode.continueWithConfigured', { provider: PROVIDER_LABELS[p] }) }}
171
159
  </UButton>
172
160
 
173
- <!-- Enter a PAT inline -->
174
- <form class="space-y-2" @submit.prevent="submitPat(patProvider, patToken.trim())">
175
- <p class="text-xs font-medium text-slate-400">{{ t('auth.localMode.enterPatTitle') }}</p>
176
- <USelect
177
- v-if="patProviderItems.length > 1"
178
- v-model="patProvider"
179
- :items="patProviderItems"
180
- size="lg"
181
- class="w-full"
182
- />
183
- <UTextarea
184
- v-model="patToken"
185
- :rows="2"
186
- :placeholder="
187
- t('auth.localMode.tokenPlaceholder', { provider: PROVIDER_LABELS[patProvider] })
188
- "
189
- class="w-full font-mono"
161
+ <!-- Neither GITHUB_PAT nor GITLAB_PAT is set: tell the developer how to configure one -->
162
+ <template v-if="!hasConfiguredPat">
163
+ <UAlert
164
+ color="warning"
165
+ variant="subtle"
166
+ icon="i-lucide-key-round"
167
+ :title="t('auth.localMode.noPatTitle')"
168
+ :description="t('auth.localMode.noPatBody')"
190
169
  />
191
- <div class="flex items-center justify-between gap-2">
170
+ <div class="flex flex-wrap gap-3 px-1">
192
171
  <a
193
- :href="tokenCreateUrl"
172
+ v-for="p in ALL_PROVIDERS"
173
+ :key="p"
174
+ :href="tokenCreateUrl(p)"
194
175
  target="_blank"
195
176
  rel="noopener noreferrer"
196
177
  class="text-xs text-indigo-400 hover:underline"
197
178
  >
198
- {{ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[patProvider] }) }}
179
+ {{ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[p] }) }}
199
180
  </a>
200
- <UButton
201
- size="lg"
202
- color="neutral"
203
- variant="subtle"
204
- type="submit"
205
- :loading="patBusy"
206
- :disabled="!patToken.trim()"
207
- >
208
- {{ t('auth.localMode.submit') }}
209
- </UButton>
210
181
  </div>
211
- </form>
182
+ </template>
183
+
212
184
  <p v-if="patError" class="text-sm text-rose-400">{{ patError }}</p>
213
185
  </div>
214
186
 
215
187
  <div
216
- v-if="showLocalLogin && auth.providers.password && mode !== 'forgot'"
188
+ v-if="isLocalMode && auth.providers.password && mode !== 'forgot'"
217
189
  class="my-4 flex items-center gap-3 text-xs text-slate-500"
218
190
  >
219
191
  <span class="h-px flex-1 bg-slate-800" /> {{ t('auth.localMode.orDivider') }}
@@ -31,6 +31,14 @@ export const useAuthStore = defineStore(
31
31
  * setup banner). Null on every other facade.
32
32
  */
33
33
  const localMode = ref<LocalModeConfig | null>(null)
34
+ /**
35
+ * Local mode only: the source-control provider the user last chose to sign in with
36
+ * (its PAT lives server-side in env — this is just the non-secret choice). Persisted, so
37
+ * on a later load we silently re-mint a session from that env PAT without showing the
38
+ * login screen. Set on an explicit sign-in, cleared on logout / 401 (so logout actually
39
+ * signs out — no auto re-login loop).
40
+ */
41
+ const autoLoginProvider = ref<'github' | 'gitlab' | null>(null)
34
42
  /** True once the initial auth handshake has settled. */
35
43
  const ready = ref(false)
36
44
 
@@ -83,6 +91,25 @@ export const useAuthStore = defineStore(
83
91
  }
84
92
  if (!user.value) token.value = null
85
93
  }
94
+
95
+ // Local mode: if no live session resolved but the user previously signed in with a
96
+ // configured env PAT, silently re-mint a session from it — so an expired/rotated token
97
+ // never forces the login screen again. The token itself stays server-side; we only
98
+ // remembered the provider choice. Guard on the provider STILL being configured (PAT could
99
+ // have been removed) and clear the choice on failure so we fall back to the login screen
100
+ // instead of looping.
101
+ if (
102
+ localMode.value?.enabled === true &&
103
+ user.value === null &&
104
+ autoLoginProvider.value &&
105
+ localMode.value.patLogin?.configured.includes(autoLoginProvider.value)
106
+ ) {
107
+ try {
108
+ await patLogin({ provider: autoLoginProvider.value })
109
+ } catch {
110
+ autoLoginProvider.value = null
111
+ }
112
+ }
86
113
  // An already-signed-in user who followed an invite link redeems it here (a
87
114
  // brand-new user redeems it server-side during signup/OAuth instead).
88
115
  if (user.value) await maybeAcceptInvite()
@@ -153,6 +180,8 @@ export const useAuthStore = defineStore(
153
180
  */
154
181
  async function patLogin(body: { provider: 'github' | 'gitlab'; token?: string }) {
155
182
  applySession(await api.patLogin(body))
183
+ // Remember the choice so a later load re-mints the session from the env PAT silently.
184
+ autoLoginProvider.value = body.provider
156
185
  }
157
186
 
158
187
  /** Request a password-reset link by email (always resolves; never reveals existence). */
@@ -170,9 +199,18 @@ export const useAuthStore = defineStore(
170
199
  api.logout().catch(() => {})
171
200
  token.value = null
172
201
  user.value = null
202
+ // Forget the remembered provider so logout sticks (otherwise bootstrap would
203
+ // immediately re-mint a session from the env PAT).
204
+ autoLoginProvider.value = null
173
205
  }
174
206
 
175
- /** Called by the API client when a request comes back 401. */
207
+ /**
208
+ * Called by the API client when a request comes back 401. Drops the dead session but KEEPS
209
+ * the remembered provider (unlike logout): a 401 from an expired/rotated token or a
210
+ * transient blip should let the next load silently re-mint from the env PAT, not force the
211
+ * login screen. The guarded re-mint in `bootstrap` clears the choice itself if it genuinely
212
+ * fails (PAT removed/revoked), so there's no re-login loop.
213
+ */
176
214
  function handleUnauthorized() {
177
215
  token.value = null
178
216
  user.value = null
@@ -184,6 +222,7 @@ export const useAuthStore = defineStore(
184
222
  required,
185
223
  providers,
186
224
  localMode,
225
+ autoLoginProvider,
187
226
  ready,
188
227
  isAuthenticated,
189
228
  needsLogin,
@@ -199,5 +238,5 @@ export const useAuthStore = defineStore(
199
238
  handleUnauthorized,
200
239
  }
201
240
  },
202
- { persist: { pick: ['token'] } },
241
+ { persist: { pick: ['token', 'autoLoginProvider'] } },
203
242
  )
@@ -752,13 +752,12 @@
752
752
  "loading": "Loading…"
753
753
  },
754
754
  "localMode": {
755
- "continueWith": "Continue with {provider}",
756
- "enterPatTitle": "Or sign in with a personal access token",
757
- "tokenPlaceholder": "paste a {provider} token",
755
+ "continueWithConfigured": "Sign in with configured {provider} PAT",
756
+ "noPatTitle": "No source-control token configured",
757
+ "noPatBody": "Set GITHUB_PAT or GITLAB_PAT in your .env to sign in with a personal access token, then restart the server.",
758
758
  "createToken": "Create a {provider} token ↗",
759
- "submit": "Sign in",
760
759
  "orDivider": "or",
761
- "failed": "Sign-in failed. Check the token and try again."
760
+ "failed": "Sign-in failed. Check that the configured token is valid and try again."
762
761
  },
763
762
  "signInRequired": {
764
763
  "personalSubscriptions": "Personal subscriptions are saved to your account, but this deployment runs without sign-in, so they can't be stored here. Use the workspace pool or a provider API key instead.",
@@ -716,13 +716,12 @@
716
716
  "loading": "Cargando…"
717
717
  },
718
718
  "localMode": {
719
- "continueWith": "Continuar con {provider}",
720
- "enterPatTitle": "O inicia sesión con un token de acceso personal",
721
- "tokenPlaceholder": "pega un token de {provider}",
719
+ "continueWithConfigured": "Inicia sesión con el PAT de {provider} configurado",
720
+ "noPatTitle": "No hay ningún token de control de versiones configurado",
721
+ "noPatBody": "Define GITHUB_PAT o GITLAB_PAT en tu .env para iniciar sesión con un token de acceso personal y reinicia el servidor.",
722
722
  "createToken": "Crear un token de {provider} ↗",
723
- "submit": "Iniciar sesión",
724
723
  "orDivider": "o",
725
- "failed": "Error al iniciar sesión. Comprueba el token e inténtalo de nuevo."
724
+ "failed": "Error al iniciar sesión. Comprueba que el token configurado sea válido e inténtalo de nuevo."
726
725
  },
727
726
  "signInRequired": {
728
727
  "personalSubscriptions": "Las suscripciones personales se guardan en tu cuenta, pero este despliegue se ejecuta sin inicio de sesión, así que no se pueden almacenar aquí. Usa el grupo del espacio de trabajo o una clave de API de proveedor.",
@@ -716,13 +716,12 @@
716
716
  "loading": "Chargement…"
717
717
  },
718
718
  "localMode": {
719
- "continueWith": "Continuer avec {provider}",
720
- "enterPatTitle": "Ou connectez-vous avec un jeton d'accès personnel",
721
- "tokenPlaceholder": "collez un jeton {provider}",
719
+ "continueWithConfigured": "Se connecter avec le PAT {provider} configuré",
720
+ "noPatTitle": "Aucun jeton de gestion de versions configuré",
721
+ "noPatBody": "Définissez GITHUB_PAT ou GITLAB_PAT dans votre .env pour vous connecter avec un jeton d'accès personnel, puis redémarrez le serveur.",
722
722
  "createToken": "Créer un jeton {provider} ↗",
723
- "submit": "Se connecter",
724
723
  "orDivider": "ou",
725
- "failed": "Échec de la connexion. Vérifiez le jeton et réessayez."
724
+ "failed": "Échec de la connexion. Vérifiez que le jeton configuré est valide et réessayez."
726
725
  },
727
726
  "signInRequired": {
728
727
  "personalSubscriptions": "Les abonnements personnels sont enregistrés sur votre compte, mais ce déploiement fonctionne sans connexion, ils ne peuvent donc pas être stockés ici. Utilisez le pool de l'espace de travail ou une clé d'API de fournisseur.",
@@ -716,13 +716,12 @@
716
716
  "loading": "Ładowanie…"
717
717
  },
718
718
  "localMode": {
719
- "continueWith": "Kontynuuj z {provider}",
720
- "enterPatTitle": "Lub zaloguj się za pomocą osobistego tokenu dostępu",
721
- "tokenPlaceholder": "wklej token {provider}",
719
+ "continueWithConfigured": "Zaloguj się skonfigurowanym tokenem PAT {provider}",
720
+ "noPatTitle": "Nie skonfigurowano tokenu systemu kontroli wersji",
721
+ "noPatBody": "Ustaw GITHUB_PAT lub GITLAB_PAT w pliku .env, aby zalogować się za pomocą osobistego tokenu dostępu, a następnie zrestartuj serwer.",
722
722
  "createToken": "Utwórz token {provider} ↗",
723
- "submit": "Zaloguj się",
724
723
  "orDivider": "lub",
725
- "failed": "Logowanie nie powiodło się. Sprawdź token i spróbuj ponownie."
724
+ "failed": "Logowanie nie powiodło się. Sprawdź, czy skonfigurowany token jest prawidłowy, i spróbuj ponownie."
726
725
  },
727
726
  "signInRequired": {
728
727
  "personalSubscriptions": "Subskrypcje osobiste są zapisywane na Twoim koncie, ale to wdrożenie działa bez logowania, więc nie można ich tutaj przechowywać. Użyj puli obszaru roboczego lub klucza API dostawcy.",
@@ -716,13 +716,12 @@
716
716
  "loading": "Завантаження…"
717
717
  },
718
718
  "localMode": {
719
- "continueWith": "Продовжити з {provider}",
720
- "enterPatTitle": "Або увійдіть за допомогою особистого токена доступу",
721
- "tokenPlaceholder": "вставте токен {provider}",
719
+ "continueWithConfigured": "Увійти за допомогою налаштованого PAT {provider}",
720
+ "noPatTitle": "Токен системи контролю версій не налаштовано",
721
+ "noPatBody": "Установіть GITHUB_PAT або GITLAB_PAT у файлі .env, щоб увійти за допомогою особистого токена доступу, потім перезапустіть сервер.",
722
722
  "createToken": "Створити токен {provider} ↗",
723
- "submit": "Увійти",
724
723
  "orDivider": "або",
725
- "failed": "Не вдалося увійти. Перевірте токен і спробуйте ще раз."
724
+ "failed": "Не вдалося увійти. Перевірте, що налаштований токен дійсний, і спробуйте ще раз."
726
725
  },
727
726
  "signInRequired": {
728
727
  "personalSubscriptions": "Особисті підписки зберігаються у вашому обліковому записі, але це розгортання працює без входу, тому їх не можна зберегти тут. Скористайтеся пулом робочого простору або ключем API постачальника.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cat-factory/app",
3
- "version": "0.47.5",
3
+ "version": "0.47.7",
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.44.0"
37
+ "@cat-factory/contracts": "0.45.1"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@toad-contracts/testing": "0.3.2",