@insureco/cli 0.1.11 → 0.1.12

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.
@@ -0,0 +1,56 @@
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: {{ include "app.fullname" . }}
5
+ labels:
6
+ {{- include "app.labels" . | nindent 4 }}
7
+ spec:
8
+ {{- if not .Values.autoscaling.enabled }}
9
+ replicas: {{ .Values.replicaCount }}
10
+ {{- end }}
11
+ selector:
12
+ matchLabels:
13
+ {{- include "app.selectorLabels" . | nindent 6 }}
14
+ template:
15
+ metadata:
16
+ labels:
17
+ {{- include "app.labels" . | nindent 8 }}
18
+ spec:
19
+ {{- with .Values.imagePullSecrets }}
20
+ imagePullSecrets:
21
+ {{- toYaml . | nindent 8 }}
22
+ {{- end }}
23
+ containers:
24
+ - name: {{ .Chart.Name }}
25
+ image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
26
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
27
+ ports:
28
+ - name: http
29
+ containerPort: {{ .Values.service.port }}
30
+ protocol: TCP
31
+ envFrom:
32
+ - configMapRef:
33
+ name: {{ include "app.fullname" . }}-config
34
+ {{- if .Values.secretRef }}
35
+ - secretRef:
36
+ name: {{ .Values.secretRef }}
37
+ optional: true
38
+ {{- end }}
39
+ livenessProbe:
40
+ {{- toYaml .Values.livenessProbe | nindent 12 }}
41
+ readinessProbe:
42
+ {{- toYaml .Values.readinessProbe | nindent 12 }}
43
+ resources:
44
+ {{- toYaml .Values.resources | nindent 12 }}
45
+ {{- with .Values.nodeSelector }}
46
+ nodeSelector:
47
+ {{- toYaml . | nindent 8 }}
48
+ {{- end }}
49
+ {{- with .Values.tolerations }}
50
+ tolerations:
51
+ {{- toYaml . | nindent 8 }}
52
+ {{- end }}
53
+ {{- with .Values.affinity }}
54
+ affinity:
55
+ {{- toYaml . | nindent 8 }}
56
+ {{- end }}
@@ -0,0 +1,41 @@
1
+ {{- if .Values.ingress.enabled -}}
2
+ apiVersion: networking.k8s.io/v1
3
+ kind: Ingress
4
+ metadata:
5
+ name: {{ include "app.fullname" . }}
6
+ labels:
7
+ {{- include "app.labels" . | nindent 4 }}
8
+ {{- with .Values.ingress.annotations }}
9
+ annotations:
10
+ {{- toYaml . | nindent 4 }}
11
+ {{- end }}
12
+ spec:
13
+ {{- if .Values.ingress.className }}
14
+ ingressClassName: {{ .Values.ingress.className }}
15
+ {{- end }}
16
+ {{- if .Values.ingress.tls }}
17
+ tls:
18
+ {{- range .Values.ingress.tls }}
19
+ - hosts:
20
+ {{- range .hosts }}
21
+ - {{ . | quote }}
22
+ {{- end }}
23
+ secretName: {{ .secretName }}
24
+ {{- end }}
25
+ {{- end }}
26
+ rules:
27
+ {{- range .Values.ingress.hosts }}
28
+ - host: {{ .host | quote }}
29
+ http:
30
+ paths:
31
+ {{- range .paths }}
32
+ - path: {{ .path }}
33
+ pathType: {{ .pathType }}
34
+ backend:
35
+ service:
36
+ name: {{ include "app.fullname" $ }}
37
+ port:
38
+ number: {{ $.Values.service.port }}
39
+ {{- end }}
40
+ {{- end }}
41
+ {{- end }}
@@ -0,0 +1,15 @@
1
+ apiVersion: v1
2
+ kind: Service
3
+ metadata:
4
+ name: {{ include "app.fullname" . }}
5
+ labels:
6
+ {{- include "app.labels" . | nindent 4 }}
7
+ spec:
8
+ type: {{ .Values.service.type }}
9
+ ports:
10
+ - port: {{ .Values.service.port }}
11
+ targetPort: http
12
+ protocol: TCP
13
+ name: http
14
+ selector:
15
+ {{- include "app.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,69 @@
1
+ replicaCount: 1
2
+
3
+ image:
4
+ repository: registry.digitalocean.com/insureco/{{name}}
5
+ pullPolicy: IfNotPresent
6
+ tag: "latest"
7
+
8
+ imagePullSecrets:
9
+ - name: insureco
10
+
11
+ nameOverride: ""
12
+ fullnameOverride: ""
13
+
14
+ service:
15
+ type: ClusterIP
16
+ port: 3000
17
+
18
+ ingress:
19
+ enabled: true
20
+ className: nginx
21
+ annotations:
22
+ nginx.ingress.kubernetes.io/proxy-body-size: "10m"
23
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
24
+ hosts:
25
+ - host: {{name}}.sandbox.tawa.insureco.io
26
+ paths:
27
+ - path: /
28
+ pathType: Prefix
29
+ tls: []
30
+
31
+ resources:
32
+ limits:
33
+ cpu: 500m
34
+ memory: 512Mi
35
+ requests:
36
+ cpu: 100m
37
+ memory: 256Mi
38
+
39
+ autoscaling:
40
+ enabled: false
41
+
42
+ env:
43
+ NODE_ENV: production
44
+ PORT: "3000"
45
+ HOSTNAME: "0.0.0.0"
46
+ BIO_ID_URL: "https://bio.tawa.insureco.io"
47
+
48
+ # K8s Secret containing OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, JWT_SECRET
49
+ # Create with: kubectl create secret generic {{name}}-secrets --from-env-file=.env -n <namespace>
50
+ secretRef: {{name}}-secrets
51
+
52
+ livenessProbe:
53
+ httpGet:
54
+ path: /api/health
55
+ port: http
56
+ initialDelaySeconds: 15
57
+ periodSeconds: 10
58
+ failureThreshold: 3
59
+
60
+ readinessProbe:
61
+ httpGet:
62
+ path: /api/health
63
+ port: http
64
+ initialDelaySeconds: 5
65
+ periodSeconds: 5
66
+
67
+ nodeSelector: {}
68
+ tolerations: []
69
+ affinity: {}
@@ -20,7 +20,7 @@
20
20
  "@types/node": "^22.0.0",
21
21
  "@types/react": "^18.3.0",
22
22
  "@types/react-dom": "^18.3.0",
23
- "eslint": "^9.0.0",
23
+ "eslint": "^8.0.0",
24
24
  "eslint-config-next": "^14.2.0",
25
25
  "typescript": "^5.7.0"
26
26
  },
@@ -1,8 +1,13 @@
1
1
  import { NextRequest, NextResponse } from 'next/server'
2
2
  import { cookies } from 'next/headers'
3
- import { exchangeCodeForTokens, setAuthCookies, sanitizeReturnTo } from '@/lib/auth'
4
-
5
- const APP_URL = process.env.APP_URL || 'http://localhost:3000'
3
+ import {
4
+ exchangeCodeForTokens,
5
+ fetchUserInfo,
6
+ createSessionToken,
7
+ setSessionCookies,
8
+ sanitizeReturnTo,
9
+ getAppUrl,
10
+ } from '@/lib/auth'
6
11
 
7
12
  export async function GET(request: NextRequest) {
8
13
  const searchParams = request.nextUrl.searchParams
@@ -23,7 +28,7 @@ export async function GET(request: NextRequest) {
23
28
 
24
29
  // Handle error from authorization server
25
30
  if (error) {
26
- const errorUrl = new URL('/', APP_URL)
31
+ const errorUrl = new URL('/', getAppUrl())
27
32
  errorUrl.searchParams.set('error', error)
28
33
  if (errorDescription) {
29
34
  errorUrl.searchParams.set('error_description', errorDescription)
@@ -33,7 +38,7 @@ export async function GET(request: NextRequest) {
33
38
 
34
39
  // Validate state to prevent CSRF
35
40
  if (!state || state !== storedState) {
36
- const errorUrl = new URL('/', APP_URL)
41
+ const errorUrl = new URL('/', getAppUrl())
37
42
  errorUrl.searchParams.set('error', 'invalid_state')
38
43
  errorUrl.searchParams.set('error_description', 'State parameter mismatch')
39
44
  return NextResponse.redirect(errorUrl)
@@ -41,19 +46,27 @@ export async function GET(request: NextRequest) {
41
46
 
42
47
  // Validate required params
43
48
  if (!code || !codeVerifier) {
44
- const errorUrl = new URL('/', APP_URL)
49
+ const errorUrl = new URL('/', getAppUrl())
45
50
  errorUrl.searchParams.set('error', 'invalid_request')
46
51
  errorUrl.searchParams.set('error_description', 'Missing authorization code')
47
52
  return NextResponse.redirect(errorUrl)
48
53
  }
49
54
 
50
55
  try {
56
+ // Exchange code for bio-id tokens
51
57
  const tokens = await exchangeCodeForTokens(code, codeVerifier)
52
- await setAuthCookies(tokens.access_token, tokens.refresh_token, tokens.expires_in)
58
+
59
+ // Fetch user info from bio-id using the access token
60
+ const user = await fetchUserInfo(tokens.access_token)
61
+
62
+ // Create our own session JWT and store it (not bio-id's access token)
63
+ const sessionToken = await createSessionToken(user)
64
+ await setSessionCookies(sessionToken, tokens.refresh_token)
65
+
53
66
  const safeReturnTo = sanitizeReturnTo(returnTo)
54
- return NextResponse.redirect(new URL(safeReturnTo, APP_URL))
67
+ return NextResponse.redirect(new URL(safeReturnTo, getAppUrl()))
55
68
  } catch (err) {
56
- const errorUrl = new URL('/', APP_URL)
69
+ const errorUrl = new URL('/', getAppUrl())
57
70
  errorUrl.searchParams.set('error', 'token_exchange_failed')
58
71
  errorUrl.searchParams.set(
59
72
  'error_description',
@@ -1,9 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { clearAuthCookies } from '@/lib/auth'
3
-
4
- const APP_URL = process.env.APP_URL || 'http://localhost:3000'
2
+ import { clearAuthCookies, getAppUrl } from '@/lib/auth'
5
3
 
6
4
  export async function POST() {
7
5
  await clearAuthCookies()
8
- return NextResponse.redirect(new URL('/', APP_URL), { status: 303 })
6
+ return NextResponse.redirect(new URL('/', getAppUrl()), { status: 303 })
9
7
  }
@@ -3,8 +3,9 @@ import { cookies } from 'next/headers'
3
3
  import {
4
4
  getCurrentUser,
5
5
  refreshAccessToken,
6
- setAuthCookies,
7
6
  fetchUserInfo,
7
+ createSessionToken,
8
+ setSessionCookies,
8
9
  COOKIE_PREFIX,
9
10
  } from '@/lib/auth'
10
11
 
@@ -18,15 +19,16 @@ export async function GET() {
18
19
  return NextResponse.json({ user }, { headers: NO_CACHE_HEADERS })
19
20
  }
20
21
 
21
- // Access token expired or missing — try refresh
22
+ // Session expired — try refreshing via bio-id refresh token
22
23
  const cookieStore = await cookies()
23
24
  const refreshToken = cookieStore.get(`${COOKIE_PREFIX}_refresh_token`)?.value
24
25
 
25
26
  if (refreshToken) {
26
27
  try {
27
28
  const tokens = await refreshAccessToken(refreshToken)
28
- await setAuthCookies(tokens.access_token, tokens.refresh_token, tokens.expires_in)
29
29
  const refreshedUser = await fetchUserInfo(tokens.access_token)
30
+ const sessionToken = await createSessionToken(refreshedUser)
31
+ await setSessionCookies(sessionToken, tokens.refresh_token)
30
32
  return NextResponse.json({ user: refreshedUser }, { headers: NO_CACHE_HEADERS })
31
33
  } catch {
32
34
  // Refresh token also invalid — user must re-login
@@ -1,13 +1,25 @@
1
1
  import { cookies } from 'next/headers'
2
- import { jwtVerify } from 'jose'
2
+ import { SignJWT, jwtVerify } from 'jose'
3
3
  import crypto from 'crypto'
4
4
 
5
- // Configuration — set these in .env
6
- const BIO_ID_URL = process.env.BIO_ID_URL || 'http://localhost:6100'
7
- const APP_URL = process.env.APP_URL || 'http://localhost:3000'
8
- const CLIENT_ID = process.env.OAUTH_CLIENT_ID || ''
9
- const CLIENT_SECRET = process.env.OAUTH_CLIENT_SECRET || ''
10
-
5
+ // Configuration — read lazily so `next build` doesn't fail when env vars are
6
+ // absent (they are injected at runtime via ConfigMap / Secret in K8s).
7
+ function getBioIdUrl(): string {
8
+ return process.env.BIO_ID_URL || 'http://localhost:6100'
9
+ }
10
+ function getAppUrl(): string {
11
+ return process.env.APP_URL || 'http://localhost:3000'
12
+ }
13
+ function getClientId(): string {
14
+ const id = process.env.OAUTH_CLIENT_ID
15
+ if (!id) throw new Error('OAUTH_CLIENT_ID is not configured')
16
+ return id
17
+ }
18
+ function getClientSecret(): string {
19
+ const secret = process.env.OAUTH_CLIENT_SECRET
20
+ if (!secret) throw new Error('OAUTH_CLIENT_SECRET is not configured')
21
+ return secret
22
+ }
11
23
  function getJwtSecret(): Uint8Array {
12
24
  const secret = process.env.JWT_SECRET
13
25
  if (!secret && process.env.NODE_ENV === 'production') {
@@ -16,26 +28,22 @@ function getJwtSecret(): Uint8Array {
16
28
  return new TextEncoder().encode(secret || 'dev-only-secret-do-not-use-in-production')
17
29
  }
18
30
 
19
- const JWT_SECRET = getJwtSecret()
20
-
21
- if (process.env.NODE_ENV === 'production' && (!CLIENT_ID || !CLIENT_SECRET)) {
22
- throw new Error('OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET are required in production')
23
- }
24
-
25
31
  const COOKIE_PREFIX = '{{name}}'
32
+ const SESSION_MAX_AGE = 60 * 60 // 1 hour
26
33
  const REFRESH_TOKEN_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
27
- const OAUTH_FLOW_MAX_AGE = 60 * 10 // 10 minutes
34
+ const OAUTH_FLOW_MAX_AGE = 60 * 10 // 10 minutes
28
35
 
29
36
  export interface User {
30
- id: string // Maps to bio_id from Bio-id userinfo endpoint
37
+ id: string
31
38
  email: string
32
39
  name: string
33
40
  roles: string[]
34
41
  }
35
42
 
36
- /**
37
- * Validate that a returnTo path is safe (relative, no open redirect)
38
- */
43
+ // ---------------------------------------------------------------------------
44
+ // Helpers
45
+ // ---------------------------------------------------------------------------
46
+
39
47
  export function sanitizeReturnTo(returnTo: string | undefined, fallback = '/dashboard'): string {
40
48
  if (!returnTo || !returnTo.startsWith('/') || returnTo.startsWith('//')) {
41
49
  return fallback
@@ -43,34 +51,26 @@ export function sanitizeReturnTo(returnTo: string | undefined, fallback = '/dash
43
51
  return returnTo
44
52
  }
45
53
 
46
- /**
47
- * Generate PKCE code verifier and S256 challenge
48
- */
49
54
  export function generatePKCE(): { codeVerifier: string; codeChallenge: string } {
50
55
  const codeVerifier = crypto.randomBytes(32).toString('base64url')
51
56
  const codeChallenge = crypto
52
57
  .createHash('sha256')
53
58
  .update(codeVerifier)
54
59
  .digest('base64url')
55
-
56
60
  return { codeVerifier, codeChallenge }
57
61
  }
58
62
 
59
- /**
60
- * Build Bio-id authorization URL
61
- */
62
63
  export function getAuthorizationUrl(state: string, codeChallenge: string): string {
63
64
  const params = new URLSearchParams({
64
- client_id: CLIENT_ID,
65
- redirect_uri: `${APP_URL}/api/auth/callback`,
65
+ client_id: getClientId(),
66
+ redirect_uri: `${getAppUrl()}/api/auth/callback`,
66
67
  response_type: 'code',
67
68
  scope: 'openid profile email',
68
69
  state,
69
70
  code_challenge: codeChallenge,
70
71
  code_challenge_method: 'S256',
71
72
  })
72
-
73
- return `${BIO_ID_URL}/oauth/authorize?${params.toString()}`
73
+ return `${getBioIdUrl()}/oauth/authorize?${params.toString()}`
74
74
  }
75
75
 
76
76
  async function parseErrorResponse(response: Response, fallbackMessage: string): Promise<string> {
@@ -82,9 +82,10 @@ async function parseErrorResponse(response: Response, fallbackMessage: string):
82
82
  }
83
83
  }
84
84
 
85
- /**
86
- * Exchange authorization code for tokens
87
- */
85
+ // ---------------------------------------------------------------------------
86
+ // Bio-id API calls
87
+ // ---------------------------------------------------------------------------
88
+
88
89
  export async function exchangeCodeForTokens(
89
90
  code: string,
90
91
  codeVerifier: string
@@ -96,15 +97,15 @@ export async function exchangeCodeForTokens(
96
97
  scope: string
97
98
  id_token?: string
98
99
  }> {
99
- const response = await fetch(`${BIO_ID_URL}/api/oauth/token`, {
100
+ const response = await fetch(`${getBioIdUrl()}/api/oauth/token`, {
100
101
  method: 'POST',
101
102
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
102
103
  body: new URLSearchParams({
103
104
  grant_type: 'authorization_code',
104
105
  code,
105
- redirect_uri: `${APP_URL}/api/auth/callback`,
106
- client_id: CLIENT_ID,
107
- client_secret: CLIENT_SECRET,
106
+ redirect_uri: `${getAppUrl()}/api/auth/callback`,
107
+ client_id: getClientId(),
108
+ client_secret: getClientSecret(),
108
109
  code_verifier: codeVerifier,
109
110
  }).toString(),
110
111
  })
@@ -116,9 +117,6 @@ export async function exchangeCodeForTokens(
116
117
  return response.json()
117
118
  }
118
119
 
119
- /**
120
- * Refresh an expired access token
121
- */
122
120
  export async function refreshAccessToken(refreshToken: string): Promise<{
123
121
  access_token: string
124
122
  token_type: string
@@ -126,14 +124,14 @@ export async function refreshAccessToken(refreshToken: string): Promise<{
126
124
  refresh_token: string
127
125
  scope: string
128
126
  }> {
129
- const response = await fetch(`${BIO_ID_URL}/api/oauth/token`, {
127
+ const response = await fetch(`${getBioIdUrl()}/api/oauth/token`, {
130
128
  method: 'POST',
131
129
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
132
130
  body: new URLSearchParams({
133
131
  grant_type: 'refresh_token',
134
132
  refresh_token: refreshToken,
135
- client_id: CLIENT_ID,
136
- client_secret: CLIENT_SECRET,
133
+ client_id: getClientId(),
134
+ client_secret: getClientSecret(),
137
135
  }).toString(),
138
136
  })
139
137
 
@@ -144,11 +142,8 @@ export async function refreshAccessToken(refreshToken: string): Promise<{
144
142
  return response.json()
145
143
  }
146
144
 
147
- /**
148
- * Fetch user info from Bio-id userinfo endpoint
149
- */
150
145
  export async function fetchUserInfo(accessToken: string): Promise<User> {
151
- const response = await fetch(`${BIO_ID_URL}/api/oauth/userinfo`, {
146
+ const response = await fetch(`${getBioIdUrl()}/api/oauth/userinfo`, {
152
147
  headers: { Authorization: `Bearer ${accessToken}` },
153
148
  })
154
149
 
@@ -166,26 +161,44 @@ export async function fetchUserInfo(accessToken: string): Promise<User> {
166
161
  }
167
162
  }
168
163
 
169
- /**
170
- * Set httpOnly auth cookies
171
- */
172
- export async function setAuthCookies(
173
- accessToken: string,
174
- refreshToken: string,
175
- expiresIn: number
164
+ // ---------------------------------------------------------------------------
165
+ // Session management
166
+ //
167
+ // After the OAuth callback, we create OUR OWN session JWT signed with
168
+ // JWT_SECRET. This avoids needing to verify bio-id's access token locally
169
+ // (bio-id uses HS256 signed with its own server secret).
170
+ // ---------------------------------------------------------------------------
171
+
172
+ export async function createSessionToken(user: User): Promise<string> {
173
+ return new SignJWT({
174
+ sub: user.id,
175
+ email: user.email,
176
+ name: user.name,
177
+ roles: user.roles,
178
+ })
179
+ .setProtectedHeader({ alg: 'HS256' })
180
+ .setIssuedAt()
181
+ .setExpirationTime(`${SESSION_MAX_AGE}s`)
182
+ .setIssuer(getAppUrl())
183
+ .sign(getJwtSecret())
184
+ }
185
+
186
+ export async function setSessionCookies(
187
+ sessionToken: string,
188
+ bioRefreshToken: string
176
189
  ): Promise<void> {
177
190
  const cookieStore = await cookies()
178
- const secure = process.env.NODE_ENV === 'production'
191
+ const secure = getAppUrl().startsWith('https://')
179
192
 
180
- cookieStore.set(`${COOKIE_PREFIX}_access_token`, accessToken, {
193
+ cookieStore.set(`${COOKIE_PREFIX}_session`, sessionToken, {
181
194
  httpOnly: true,
182
195
  secure,
183
196
  sameSite: 'lax',
184
- maxAge: expiresIn,
197
+ maxAge: SESSION_MAX_AGE,
185
198
  path: '/',
186
199
  })
187
200
 
188
- cookieStore.set(`${COOKIE_PREFIX}_refresh_token`, refreshToken, {
201
+ cookieStore.set(`${COOKIE_PREFIX}_refresh_token`, bioRefreshToken, {
189
202
  httpOnly: true,
190
203
  secure,
191
204
  sameSite: 'lax',
@@ -194,12 +207,9 @@ export async function setAuthCookies(
194
207
  })
195
208
  }
196
209
 
197
- /**
198
- * Clear all auth cookies
199
- */
200
210
  export async function clearAuthCookies(): Promise<void> {
201
211
  const cookieStore = await cookies()
202
- cookieStore.delete(`${COOKIE_PREFIX}_access_token`)
212
+ cookieStore.delete(`${COOKIE_PREFIX}_session`)
203
213
  cookieStore.delete(`${COOKIE_PREFIX}_refresh_token`)
204
214
  cookieStore.delete('oauth_state')
205
215
  cookieStore.delete('oauth_code_verifier')
@@ -207,23 +217,24 @@ export async function clearAuthCookies(): Promise<void> {
207
217
  }
208
218
 
209
219
  /**
210
- * Get the current authenticated user from JWT payload (no network call)
220
+ * Get the current user from the session cookie (no network call).
221
+ * Verifies OUR session JWT — not bio-id's access token.
211
222
  */
212
223
  export async function getCurrentUser(): Promise<User | null> {
213
224
  const cookieStore = await cookies()
214
- const accessToken = cookieStore.get(`${COOKIE_PREFIX}_access_token`)?.value
225
+ const sessionToken = cookieStore.get(`${COOKIE_PREFIX}_session`)?.value
215
226
 
216
- if (!accessToken) {
227
+ if (!sessionToken) {
217
228
  return null
218
229
  }
219
230
 
220
231
  try {
221
- const { payload } = await jwtVerify(accessToken, JWT_SECRET, {
222
- issuer: BIO_ID_URL,
232
+ const { payload } = await jwtVerify(sessionToken, getJwtSecret(), {
233
+ issuer: getAppUrl(),
223
234
  })
224
235
 
225
236
  return {
226
- id: (payload.bio_id as string) || payload.sub || '',
237
+ id: payload.sub || '',
227
238
  email: payload.email as string,
228
239
  name: payload.name as string,
229
240
  roles: (payload.roles as string[]) || [],
@@ -238,7 +249,7 @@ export async function getCurrentUser(): Promise<User | null> {
238
249
  */
239
250
  export async function getLoginUrl(returnTo?: string): Promise<string> {
240
251
  const cookieStore = await cookies()
241
- const secure = process.env.NODE_ENV === 'production'
252
+ const secure = getAppUrl().startsWith('https://')
242
253
 
243
254
  const state = crypto.randomBytes(16).toString('hex')
244
255
  const { codeVerifier, codeChallenge } = generatePKCE()
@@ -271,4 +282,4 @@ export async function getLoginUrl(returnTo?: string): Promise<string> {
271
282
  return getAuthorizationUrl(state, codeChallenge)
272
283
  }
273
284
 
274
- export { COOKIE_PREFIX, JWT_SECRET, BIO_ID_URL }
285
+ export { COOKIE_PREFIX, getBioIdUrl, getAppUrl }
@@ -36,24 +36,25 @@ export async function middleware(request: NextRequest) {
36
36
  return NextResponse.next()
37
37
  }
38
38
 
39
- const accessToken = request.cookies.get(`${COOKIE_PREFIX}_access_token`)?.value
39
+ // Check for our session cookie (not bio-id's access token)
40
+ const sessionToken = request.cookies.get(`${COOKIE_PREFIX}_session`)?.value
40
41
 
41
- if (!accessToken) {
42
+ if (!sessionToken) {
42
43
  const loginUrl = new URL('/api/auth/login', request.url)
43
44
  loginUrl.searchParams.set('returnTo', pathname)
44
45
  return NextResponse.redirect(loginUrl)
45
46
  }
46
47
 
47
- // Verify JWT is valid and not expired
48
+ // Verify our own session JWT (signed with JWT_SECRET, issued by APP_URL)
48
49
  try {
49
50
  const secret = new TextEncoder().encode(
50
51
  process.env.JWT_SECRET || 'dev-only-secret-do-not-use-in-production'
51
52
  )
52
- await jwtVerify(accessToken, secret, {
53
- issuer: process.env.BIO_ID_URL || 'http://localhost:6100',
53
+ await jwtVerify(sessionToken, secret, {
54
+ issuer: process.env.APP_URL || 'http://localhost:3000',
54
55
  })
55
56
  } catch {
56
- // Token invalid or expired — redirect to login
57
+ // Session expired or invalid — redirect to login
57
58
  const loginUrl = new URL('/api/auth/login', request.url)
58
59
  loginUrl.searchParams.set('returnTo', pathname)
59
60
  return NextResponse.redirect(loginUrl)
@@ -278,17 +278,21 @@ export interface Version {
278
278
  updatedAt: string;
279
279
  }
280
280
  export interface PushRequest {
281
- serviceId: string;
282
281
  serviceName: string;
283
282
  archive: Buffer;
284
- checksum: string;
283
+ environment?: string;
284
+ branch?: string;
285
285
  message?: string;
286
286
  }
287
287
  export interface PushResponse {
288
- versionId: string;
289
- archiveId: string;
288
+ buildId: string;
289
+ serviceId: string;
290
+ serviceName: string;
291
+ version: string;
292
+ environment: string;
293
+ tarballId: string;
294
+ status: string;
290
295
  message: string;
291
- size: number;
292
296
  }
293
297
  export interface DeployVersionResponse {
294
298
  buildId: string;