@insureco/cli 0.1.10 → 0.1.11

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 (49) hide show
  1. package/dist/commands/deploy.d.ts.map +1 -1
  2. package/dist/commands/deploy.js +6 -52
  3. package/dist/commands/deploy.js.map +1 -1
  4. package/dist/commands/push.d.ts.map +1 -1
  5. package/dist/commands/push.js +10 -1
  6. package/dist/commands/push.js.map +1 -1
  7. package/dist/commands/rollback.d.ts.map +1 -1
  8. package/dist/commands/rollback.js +10 -35
  9. package/dist/commands/rollback.js.map +1 -1
  10. package/dist/commands/sample.d.ts +2 -0
  11. package/dist/commands/sample.d.ts.map +1 -1
  12. package/dist/commands/sample.js +140 -12
  13. package/dist/commands/sample.js.map +1 -1
  14. package/dist/commands/versions.d.ts.map +1 -1
  15. package/dist/commands/versions.js +10 -56
  16. package/dist/commands/versions.js.map +1 -1
  17. package/dist/index.js +2 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/lib/builder.d.ts.map +1 -1
  20. package/dist/lib/builder.js +46 -3
  21. package/dist/lib/builder.js.map +1 -1
  22. package/dist/lib/watch.d.ts +26 -0
  23. package/dist/lib/watch.d.ts.map +1 -0
  24. package/dist/lib/watch.js +136 -0
  25. package/dist/lib/watch.js.map +1 -0
  26. package/dist/templates/nextjs-oauth/.env.example +14 -0
  27. package/dist/templates/nextjs-oauth/Dockerfile +51 -0
  28. package/dist/templates/nextjs-oauth/README.md +128 -0
  29. package/dist/templates/nextjs-oauth/catalog-info.yaml +17 -0
  30. package/dist/templates/nextjs-oauth/helm/{{name}}/Chart.yaml +9 -0
  31. package/dist/templates/nextjs-oauth/helm/{{name}}/templates/deployment.yaml +68 -0
  32. package/dist/templates/nextjs-oauth/helm/{{name}}/templates/service.yaml +17 -0
  33. package/dist/templates/nextjs-oauth/helm/{{name}}/values.yaml +51 -0
  34. package/dist/templates/nextjs-oauth/next.config.js +23 -0
  35. package/dist/templates/nextjs-oauth/package.json +30 -0
  36. package/dist/templates/nextjs-oauth/public/.gitkeep +0 -0
  37. package/dist/templates/nextjs-oauth/src/app/api/auth/callback/route.ts +64 -0
  38. package/dist/templates/nextjs-oauth/src/app/api/auth/login/route.ts +16 -0
  39. package/dist/templates/nextjs-oauth/src/app/api/auth/logout/route.ts +9 -0
  40. package/dist/templates/nextjs-oauth/src/app/api/auth/session/route.ts +40 -0
  41. package/dist/templates/nextjs-oauth/src/app/api/example/route.ts +63 -0
  42. package/dist/templates/nextjs-oauth/src/app/api/health/route.ts +10 -0
  43. package/dist/templates/nextjs-oauth/src/app/dashboard/page.tsx +92 -0
  44. package/dist/templates/nextjs-oauth/src/app/layout.tsx +18 -0
  45. package/dist/templates/nextjs-oauth/src/app/page.tsx +110 -0
  46. package/dist/templates/nextjs-oauth/src/lib/auth.ts +274 -0
  47. package/dist/templates/nextjs-oauth/src/middleware.ts +70 -0
  48. package/dist/templates/nextjs-oauth/tsconfig.json +26 -0
  49. package/package.json +1 -1
@@ -0,0 +1,274 @@
1
+ import { cookies } from 'next/headers'
2
+ import { jwtVerify } from 'jose'
3
+ import crypto from 'crypto'
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
+
11
+ function getJwtSecret(): Uint8Array {
12
+ const secret = process.env.JWT_SECRET
13
+ if (!secret && process.env.NODE_ENV === 'production') {
14
+ throw new Error('JWT_SECRET environment variable is required in production')
15
+ }
16
+ return new TextEncoder().encode(secret || 'dev-only-secret-do-not-use-in-production')
17
+ }
18
+
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
+ const COOKIE_PREFIX = '{{name}}'
26
+ const REFRESH_TOKEN_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
27
+ const OAUTH_FLOW_MAX_AGE = 60 * 10 // 10 minutes
28
+
29
+ export interface User {
30
+ id: string // Maps to bio_id from Bio-id userinfo endpoint
31
+ email: string
32
+ name: string
33
+ roles: string[]
34
+ }
35
+
36
+ /**
37
+ * Validate that a returnTo path is safe (relative, no open redirect)
38
+ */
39
+ export function sanitizeReturnTo(returnTo: string | undefined, fallback = '/dashboard'): string {
40
+ if (!returnTo || !returnTo.startsWith('/') || returnTo.startsWith('//')) {
41
+ return fallback
42
+ }
43
+ return returnTo
44
+ }
45
+
46
+ /**
47
+ * Generate PKCE code verifier and S256 challenge
48
+ */
49
+ export function generatePKCE(): { codeVerifier: string; codeChallenge: string } {
50
+ const codeVerifier = crypto.randomBytes(32).toString('base64url')
51
+ const codeChallenge = crypto
52
+ .createHash('sha256')
53
+ .update(codeVerifier)
54
+ .digest('base64url')
55
+
56
+ return { codeVerifier, codeChallenge }
57
+ }
58
+
59
+ /**
60
+ * Build Bio-id authorization URL
61
+ */
62
+ export function getAuthorizationUrl(state: string, codeChallenge: string): string {
63
+ const params = new URLSearchParams({
64
+ client_id: CLIENT_ID,
65
+ redirect_uri: `${APP_URL}/api/auth/callback`,
66
+ response_type: 'code',
67
+ scope: 'openid profile email',
68
+ state,
69
+ code_challenge: codeChallenge,
70
+ code_challenge_method: 'S256',
71
+ })
72
+
73
+ return `${BIO_ID_URL}/oauth/authorize?${params.toString()}`
74
+ }
75
+
76
+ async function parseErrorResponse(response: Response, fallbackMessage: string): Promise<string> {
77
+ try {
78
+ const error = await response.json()
79
+ return error.error_description || error.error || fallbackMessage
80
+ } catch {
81
+ return fallbackMessage
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Exchange authorization code for tokens
87
+ */
88
+ export async function exchangeCodeForTokens(
89
+ code: string,
90
+ codeVerifier: string
91
+ ): Promise<{
92
+ access_token: string
93
+ token_type: string
94
+ expires_in: number
95
+ refresh_token: string
96
+ scope: string
97
+ id_token?: string
98
+ }> {
99
+ const response = await fetch(`${BIO_ID_URL}/api/oauth/token`, {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
102
+ body: new URLSearchParams({
103
+ grant_type: 'authorization_code',
104
+ code,
105
+ redirect_uri: `${APP_URL}/api/auth/callback`,
106
+ client_id: CLIENT_ID,
107
+ client_secret: CLIENT_SECRET,
108
+ code_verifier: codeVerifier,
109
+ }).toString(),
110
+ })
111
+
112
+ if (!response.ok) {
113
+ throw new Error(await parseErrorResponse(response, 'Token exchange failed'))
114
+ }
115
+
116
+ return response.json()
117
+ }
118
+
119
+ /**
120
+ * Refresh an expired access token
121
+ */
122
+ export async function refreshAccessToken(refreshToken: string): Promise<{
123
+ access_token: string
124
+ token_type: string
125
+ expires_in: number
126
+ refresh_token: string
127
+ scope: string
128
+ }> {
129
+ const response = await fetch(`${BIO_ID_URL}/api/oauth/token`, {
130
+ method: 'POST',
131
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
132
+ body: new URLSearchParams({
133
+ grant_type: 'refresh_token',
134
+ refresh_token: refreshToken,
135
+ client_id: CLIENT_ID,
136
+ client_secret: CLIENT_SECRET,
137
+ }).toString(),
138
+ })
139
+
140
+ if (!response.ok) {
141
+ throw new Error(await parseErrorResponse(response, 'Token refresh failed'))
142
+ }
143
+
144
+ return response.json()
145
+ }
146
+
147
+ /**
148
+ * Fetch user info from Bio-id userinfo endpoint
149
+ */
150
+ export async function fetchUserInfo(accessToken: string): Promise<User> {
151
+ const response = await fetch(`${BIO_ID_URL}/api/oauth/userinfo`, {
152
+ headers: { Authorization: `Bearer ${accessToken}` },
153
+ })
154
+
155
+ if (!response.ok) {
156
+ throw new Error('Failed to fetch user info')
157
+ }
158
+
159
+ const data = await response.json()
160
+
161
+ return {
162
+ id: data.bio_id || data.sub,
163
+ email: data.email,
164
+ name: data.name,
165
+ roles: data.roles || [],
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Set httpOnly auth cookies
171
+ */
172
+ export async function setAuthCookies(
173
+ accessToken: string,
174
+ refreshToken: string,
175
+ expiresIn: number
176
+ ): Promise<void> {
177
+ const cookieStore = await cookies()
178
+ const secure = process.env.NODE_ENV === 'production'
179
+
180
+ cookieStore.set(`${COOKIE_PREFIX}_access_token`, accessToken, {
181
+ httpOnly: true,
182
+ secure,
183
+ sameSite: 'lax',
184
+ maxAge: expiresIn,
185
+ path: '/',
186
+ })
187
+
188
+ cookieStore.set(`${COOKIE_PREFIX}_refresh_token`, refreshToken, {
189
+ httpOnly: true,
190
+ secure,
191
+ sameSite: 'lax',
192
+ maxAge: REFRESH_TOKEN_MAX_AGE,
193
+ path: '/',
194
+ })
195
+ }
196
+
197
+ /**
198
+ * Clear all auth cookies
199
+ */
200
+ export async function clearAuthCookies(): Promise<void> {
201
+ const cookieStore = await cookies()
202
+ cookieStore.delete(`${COOKIE_PREFIX}_access_token`)
203
+ cookieStore.delete(`${COOKIE_PREFIX}_refresh_token`)
204
+ cookieStore.delete('oauth_state')
205
+ cookieStore.delete('oauth_code_verifier')
206
+ cookieStore.delete('oauth_return_to')
207
+ }
208
+
209
+ /**
210
+ * Get the current authenticated user from JWT payload (no network call)
211
+ */
212
+ export async function getCurrentUser(): Promise<User | null> {
213
+ const cookieStore = await cookies()
214
+ const accessToken = cookieStore.get(`${COOKIE_PREFIX}_access_token`)?.value
215
+
216
+ if (!accessToken) {
217
+ return null
218
+ }
219
+
220
+ try {
221
+ const { payload } = await jwtVerify(accessToken, JWT_SECRET, {
222
+ issuer: BIO_ID_URL,
223
+ })
224
+
225
+ return {
226
+ id: (payload.bio_id as string) || payload.sub || '',
227
+ email: payload.email as string,
228
+ name: payload.name as string,
229
+ roles: (payload.roles as string[]) || [],
230
+ }
231
+ } catch {
232
+ return null
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Generate a login URL with PKCE and state stored in cookies
238
+ */
239
+ export async function getLoginUrl(returnTo?: string): Promise<string> {
240
+ const cookieStore = await cookies()
241
+ const secure = process.env.NODE_ENV === 'production'
242
+
243
+ const state = crypto.randomBytes(16).toString('hex')
244
+ const { codeVerifier, codeChallenge } = generatePKCE()
245
+
246
+ cookieStore.set('oauth_state', state, {
247
+ httpOnly: true,
248
+ secure,
249
+ sameSite: 'lax',
250
+ maxAge: OAUTH_FLOW_MAX_AGE,
251
+ path: '/',
252
+ })
253
+
254
+ cookieStore.set('oauth_code_verifier', codeVerifier, {
255
+ httpOnly: true,
256
+ secure,
257
+ sameSite: 'lax',
258
+ maxAge: OAUTH_FLOW_MAX_AGE,
259
+ path: '/',
260
+ })
261
+
262
+ const safeReturnTo = sanitizeReturnTo(returnTo)
263
+ cookieStore.set('oauth_return_to', safeReturnTo, {
264
+ httpOnly: true,
265
+ secure,
266
+ sameSite: 'lax',
267
+ maxAge: OAUTH_FLOW_MAX_AGE,
268
+ path: '/',
269
+ })
270
+
271
+ return getAuthorizationUrl(state, codeChallenge)
272
+ }
273
+
274
+ export { COOKIE_PREFIX, JWT_SECRET, BIO_ID_URL }
@@ -0,0 +1,70 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import { jwtVerify } from 'jose'
3
+
4
+ const COOKIE_PREFIX = '{{name}}'
5
+
6
+ // Routes that require authentication
7
+ const PROTECTED_PATHS = ['/dashboard']
8
+
9
+ // Routes that should never be blocked
10
+ const PUBLIC_PATHS = [
11
+ '/',
12
+ '/api/auth',
13
+ '/api/health',
14
+ ]
15
+
16
+ function isPublicPath(pathname: string): boolean {
17
+ return PUBLIC_PATHS.some(
18
+ (path) => pathname === path || pathname.startsWith(`${path}/`)
19
+ )
20
+ }
21
+
22
+ function isProtectedPath(pathname: string): boolean {
23
+ return PROTECTED_PATHS.some(
24
+ (path) => pathname === path || pathname.startsWith(`${path}/`)
25
+ )
26
+ }
27
+
28
+ export async function middleware(request: NextRequest) {
29
+ const { pathname } = request.nextUrl
30
+
31
+ if (isPublicPath(pathname)) {
32
+ return NextResponse.next()
33
+ }
34
+
35
+ if (!isProtectedPath(pathname)) {
36
+ return NextResponse.next()
37
+ }
38
+
39
+ const accessToken = request.cookies.get(`${COOKIE_PREFIX}_access_token`)?.value
40
+
41
+ if (!accessToken) {
42
+ const loginUrl = new URL('/api/auth/login', request.url)
43
+ loginUrl.searchParams.set('returnTo', pathname)
44
+ return NextResponse.redirect(loginUrl)
45
+ }
46
+
47
+ // Verify JWT is valid and not expired
48
+ try {
49
+ const secret = new TextEncoder().encode(
50
+ process.env.JWT_SECRET || 'dev-only-secret-do-not-use-in-production'
51
+ )
52
+ await jwtVerify(accessToken, secret, {
53
+ issuer: process.env.BIO_ID_URL || 'http://localhost:6100',
54
+ })
55
+ } catch {
56
+ // Token invalid or expired — redirect to login
57
+ const loginUrl = new URL('/api/auth/login', request.url)
58
+ loginUrl.searchParams.set('returnTo', pathname)
59
+ return NextResponse.redirect(loginUrl)
60
+ }
61
+
62
+ return NextResponse.next()
63
+ }
64
+
65
+ export const config = {
66
+ matcher: [
67
+ // Match all paths except static files and Next.js internals
68
+ '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
69
+ ],
70
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "lib": ["dom", "dom.iterable", "esnext"],
4
+ "allowJs": true,
5
+ "skipLibCheck": true,
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "esModuleInterop": true,
9
+ "module": "esnext",
10
+ "moduleResolution": "bundler",
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "jsx": "preserve",
14
+ "incremental": true,
15
+ "plugins": [
16
+ {
17
+ "name": "next"
18
+ }
19
+ ],
20
+ "paths": {
21
+ "@/*": ["./src/*"]
22
+ }
23
+ },
24
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25
+ "exclude": ["node_modules"]
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@insureco/cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Developer CLI for InsurEco Tawa platform",
5
5
  "type": "module",
6
6
  "bin": {