@stjoseph/prumo 0.0.5 → 0.1.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.
Files changed (155) hide show
  1. package/.prumo-templates/api/auth.md +51 -95
  2. package/.prumo-templates/api/config.md +30 -56
  3. package/.prumo-templates/api/errors.md +64 -44
  4. package/.prumo-templates/api/logging.md +27 -44
  5. package/.prumo-templates/api/observability.md +36 -62
  6. package/.prumo-templates/api/pagination.md +23 -38
  7. package/.prumo-templates/api/routes.md +67 -48
  8. package/.prumo-templates/api/structure.md +130 -0
  9. package/.prumo-templates/client/data.md +4 -3
  10. package/.prumo-templates/core/conventions.md +10 -7
  11. package/.prumo-templates/core/testing.md +43 -26
  12. package/.prumo-templates/core/tooling.md +33 -30
  13. package/.prumo-templates/database/entities.md +53 -61
  14. package/.prumo-templates/database/queries.md +34 -47
  15. package/.prumo-templates/database/transactions.md +28 -38
  16. package/.prumo-templates/mcp/server.md +54 -0
  17. package/.prumo-templates/monorepo/dependencies.md +3 -3
  18. package/.prumo-templates/monorepo/layout.md +1 -1
  19. package/README.md +12 -6
  20. package/dist/asker.js +4 -0
  21. package/dist/clean.js +2 -2
  22. package/dist/cli.js +4 -0
  23. package/dist/commands.js +2 -0
  24. package/dist/compose.js +12 -8
  25. package/dist/context.js +2 -0
  26. package/dist/generate.js +8 -0
  27. package/dist/mcp.js +100 -0
  28. package/dist/questions.js +21 -1
  29. package/package.json +4 -3
  30. package/templates/api/.swcrc +13 -0
  31. package/templates/api/README.md +10 -1
  32. package/templates/api/biome.jsonc +5 -3
  33. package/templates/api/package.json +22 -28
  34. package/templates/api/pnpm-workspace.yaml +0 -2
  35. package/templates/api/scripts/ports.mjs +508 -0
  36. package/templates/api/src/domain/shared/errors/domain-error.ts +5 -0
  37. package/templates/api/src/domain/shared/transactions/transaction-manager.ts +5 -0
  38. package/templates/api/src/domain/users/dto/profile-response.dto.ts +14 -0
  39. package/templates/api/src/domain/users/dto/update-profile.dto.ts +10 -0
  40. package/templates/api/src/domain/users/entities/profile.entity.ts +26 -0
  41. package/templates/api/src/domain/users/errors/profile-not-found.error.ts +9 -0
  42. package/templates/api/src/domain/users/repositories/profile.repository.ts +8 -0
  43. package/templates/api/src/domain/users/use-cases/create-profile.use-case.ts +19 -0
  44. package/templates/api/src/domain/users/use-cases/find-profile.use-case.ts +22 -0
  45. package/templates/api/src/domain/users/use-cases/update-profile.use-case.ts +26 -0
  46. package/templates/api/src/infra/auth/auth.cli.ts +5 -0
  47. package/templates/api/src/{auth → infra/auth}/auth.factory.ts +26 -1
  48. package/templates/api/src/infra/config/env.ts +47 -0
  49. package/templates/api/src/infra/database/mikroorm/entities/auth-user.schema.ts +16 -0
  50. package/templates/api/src/infra/database/mikroorm/entities/profile.schema.ts +28 -0
  51. package/templates/api/src/{mikro-orm.config.ts → infra/database/mikroorm/mikro-orm.config.ts} +1 -1
  52. package/templates/api/src/infra/database/mikroorm/mikro-orm.factory.ts +17 -0
  53. package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-profile.repository.ts +17 -0
  54. package/templates/api/src/infra/database/mikroorm/transactions/mikroorm-transaction-manager.ts +14 -0
  55. package/templates/api/src/infra/di/index.ts +32 -0
  56. package/templates/api/src/infra/di/users.di.ts +7 -0
  57. package/templates/api/src/infra/http/app.ts +99 -0
  58. package/templates/api/src/infra/http/controllers/auth.controller.ts +36 -0
  59. package/templates/api/src/infra/http/controllers/health.controller.ts +19 -0
  60. package/templates/api/src/infra/http/controllers/mcp.controller.ts +40 -0
  61. package/templates/api/src/infra/http/controllers/users.controller.ts +35 -0
  62. package/templates/api/src/infra/http/errors/error-handler.ts +116 -0
  63. package/templates/api/src/infra/http/errors/http-error.ts +8 -0
  64. package/templates/api/src/infra/http/fastify.d.ts +17 -0
  65. package/templates/api/src/infra/http/fetch-bridge.ts +42 -0
  66. package/templates/api/src/infra/http/hooks/auth.hook.ts +32 -0
  67. package/templates/api/src/infra/http/hooks/request-context.hook.ts +9 -0
  68. package/templates/api/src/infra/http/server.ts +34 -0
  69. package/templates/api/src/infra/mcp/mcp.server.ts +20 -0
  70. package/templates/api/src/infra/mcp/tools/users.tool.ts +39 -0
  71. package/templates/api/test/domain/users/use-cases/find-profile.use-case.spec.ts +22 -0
  72. package/templates/api/test/domain/users/use-cases/update-profile.use-case.spec.ts +26 -0
  73. package/templates/api/{src → test/infra}/auth/auth.factory.spec.ts +4 -11
  74. package/templates/api/test/infra/database/mikroorm/mikro-orm.factory.spec.ts +8 -0
  75. package/templates/api/test/infra/database/mikroorm/repositories/mikroorm-profile.repository.spec.ts +51 -0
  76. package/templates/api/test/infra/database/mikroorm/transactions/mikroorm-transaction-manager.spec.ts +29 -0
  77. package/templates/api/test/infra/http/app.spec.ts +21 -0
  78. package/templates/api/test/infra/http/controllers/mcp.controller.spec.ts +152 -0
  79. package/templates/api/test/infra/http/controllers/users.controller.spec.ts +105 -0
  80. package/templates/api/test/support/factories/profile.factory.ts +25 -0
  81. package/templates/api/test/{factories → support/factories}/user.factory.ts +1 -1
  82. package/templates/api/test/support/fakes/in-memory-profile.repository.ts +14 -0
  83. package/templates/api/test/{setup.ts → support/setup.ts} +26 -14
  84. package/templates/api/tsconfig.json +3 -6
  85. package/templates/api/vitest.config.mts +4 -4
  86. package/templates/mobile/.env.example +3 -0
  87. package/templates/mobile/README.md +10 -0
  88. package/templates/mobile/jest.config.js +5 -1
  89. package/templates/mobile/package.json +3 -3
  90. package/templates/mobile/scripts/ports.mjs +508 -0
  91. package/templates/mobile/{src → test}/features/auth/authenticated-layout.spec.tsx +1 -1
  92. package/templates/mobile/{src → test}/features/auth/sign-in-form.spec.tsx +2 -2
  93. package/templates/mobile/{src → test}/features/auth/sign-out-button.spec.tsx +2 -2
  94. package/templates/mobile/{src → test}/features/profile/profile-form.spec.tsx +2 -2
  95. package/templates/mobile/tsconfig.json +2 -1
  96. package/templates/site/.env.example +2 -0
  97. package/templates/site/README.md +12 -1
  98. package/templates/site/gitignore +1 -0
  99. package/templates/site/package.json +1 -1
  100. package/templates/site/scripts/ports.mjs +508 -0
  101. package/templates/site/{src → test}/app/page.spec.ts +1 -1
  102. package/templates/site/tsconfig.json +2 -1
  103. package/templates/site/vitest.config.mts +4 -2
  104. package/templates/web/.env.example +3 -0
  105. package/templates/web/README.md +10 -1
  106. package/templates/web/package.json +5 -3
  107. package/templates/web/scripts/ports.mjs +508 -0
  108. package/templates/web/src/features/auth/auth-client.ts +5 -0
  109. package/templates/web/src/features/auth/sign-in-form.tsx +10 -3
  110. package/templates/web/src/features/oauth/consent-form.tsx +63 -0
  111. package/templates/web/src/routeTree.gen.ts +21 -2
  112. package/templates/web/src/routes/_authenticated/consent.tsx +21 -0
  113. package/templates/web/{src → test}/app.spec.tsx +2 -2
  114. package/templates/web/{src → test}/features/auth/sign-in-form.spec.tsx +2 -2
  115. package/templates/web/test/features/oauth/consent-form.spec.tsx +45 -0
  116. package/templates/web/{src → test}/features/profile/profile-form.spec.tsx +2 -2
  117. package/templates/web/test/{render-app.tsx → support/render-app.tsx} +1 -1
  118. package/templates/web/tsconfig.json +2 -1
  119. package/templates/web/vite.config.ts +3 -3
  120. package/templates/workspace/README.md +7 -3
  121. package/templates/workspace/scripts/ports.mjs +508 -0
  122. package/.prumo-templates/api/modules.md +0 -117
  123. package/templates/api/nest-cli.json +0 -10
  124. package/templates/api/src/app.module.ts +0 -46
  125. package/templates/api/src/auth/auth.constants.ts +0 -2
  126. package/templates/api/src/auth/auth.controller.ts +0 -19
  127. package/templates/api/src/auth/auth.guard.ts +0 -43
  128. package/templates/api/src/auth/auth.module.ts +0 -30
  129. package/templates/api/src/auth/auth.ts +0 -4
  130. package/templates/api/src/auth/current-user.decorator.ts +0 -9
  131. package/templates/api/src/auth/express.d.ts +0 -10
  132. package/templates/api/src/auth/public.decorator.ts +0 -4
  133. package/templates/api/src/config/env.module.ts +0 -11
  134. package/templates/api/src/config/env.ts +0 -81
  135. package/templates/api/src/errors/problem-details.filter.ts +0 -86
  136. package/templates/api/src/errors/validation-exception.factory.ts +0 -30
  137. package/templates/api/src/health/health.controller.ts +0 -32
  138. package/templates/api/src/health/health.module.ts +0 -9
  139. package/templates/api/src/main.ts +0 -43
  140. package/templates/api/src/mikro-orm.factory.ts +0 -14
  141. package/templates/api/src/request-context/request-context.module.ts +0 -9
  142. package/templates/api/src/request-context/request-context.service.ts +0 -17
  143. package/templates/api/src/request-context/request-id.middleware.ts +0 -19
  144. package/templates/api/src/users/dto/update-profile.dto.ts +0 -20
  145. package/templates/api/src/users/entities/profile.entity.ts +0 -27
  146. package/templates/api/src/users/users.controller.ts +0 -23
  147. package/templates/api/src/users/users.module.ts +0 -10
  148. package/templates/api/src/users/users.service.spec.ts +0 -44
  149. package/templates/api/src/users/users.service.ts +0 -36
  150. package/templates/api/test/factories/profile.factory.ts +0 -26
  151. package/templates/api/tsconfig.build.json +0 -8
  152. /package/templates/api/test/{global-setup.ts → support/global-setup.ts} +0 -0
  153. /package/templates/mobile/test/{fake-transport.ts → support/fake-transport.ts} +0 -0
  154. /package/templates/web/test/{fake-transport.ts → support/fake-transport.ts} +0 -0
  155. /package/templates/web/test/{setup.ts → support/setup.ts} +0 -0
@@ -13,6 +13,7 @@ import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
13
13
  import { Route as SignInRouteImport } from './routes/sign-in'
14
14
  import { Route as SignUpRouteImport } from './routes/sign-up'
15
15
  import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/index'
16
+ import { Route as AuthenticatedConsentRouteImport } from './routes/_authenticated/consent'
16
17
 
17
18
  const AuthenticatedRoute = AuthenticatedRouteImport.update({
18
19
  id: '/_authenticated',
@@ -33,15 +34,22 @@ const AuthenticatedIndexRoute = AuthenticatedIndexRouteImport.update({
33
34
  path: '/',
34
35
  getParentRoute: () => AuthenticatedRoute,
35
36
  } as any)
37
+ const AuthenticatedConsentRoute = AuthenticatedConsentRouteImport.update({
38
+ id: '/consent',
39
+ path: '/consent',
40
+ getParentRoute: () => AuthenticatedRoute,
41
+ } as any)
36
42
 
37
43
  export interface FileRoutesByFullPath {
38
44
  '/': typeof AuthenticatedIndexRoute
39
45
  '/sign-in': typeof SignInRoute
40
46
  '/sign-up': typeof SignUpRoute
47
+ '/consent': typeof AuthenticatedConsentRoute
41
48
  }
42
49
  export interface FileRoutesByTo {
43
50
  '/sign-in': typeof SignInRoute
44
51
  '/sign-up': typeof SignUpRoute
52
+ '/consent': typeof AuthenticatedConsentRoute
45
53
  '/': typeof AuthenticatedIndexRoute
46
54
  }
47
55
  export interface FileRoutesById {
@@ -49,18 +57,20 @@ export interface FileRoutesById {
49
57
  '/_authenticated': typeof AuthenticatedRouteWithChildren
50
58
  '/sign-in': typeof SignInRoute
51
59
  '/sign-up': typeof SignUpRoute
60
+ '/_authenticated/consent': typeof AuthenticatedConsentRoute
52
61
  '/_authenticated/': typeof AuthenticatedIndexRoute
53
62
  }
54
63
  export interface FileRouteTypes {
55
64
  fileRoutesByFullPath: FileRoutesByFullPath
56
- fullPaths: '/' | '/sign-in' | '/sign-up'
65
+ fullPaths: '/' | '/sign-in' | '/sign-up' | '/consent'
57
66
  fileRoutesByTo: FileRoutesByTo
58
- to: '/sign-in' | '/sign-up' | '/'
67
+ to: '/sign-in' | '/sign-up' | '/consent' | '/'
59
68
  id:
60
69
  | '__root__'
61
70
  | '/_authenticated'
62
71
  | '/sign-in'
63
72
  | '/sign-up'
73
+ | '/_authenticated/consent'
64
74
  | '/_authenticated/'
65
75
  fileRoutesById: FileRoutesById
66
76
  }
@@ -100,14 +110,23 @@ declare module '@tanstack/react-router' {
100
110
  preLoaderRoute: typeof AuthenticatedIndexRouteImport
101
111
  parentRoute: typeof AuthenticatedRoute
102
112
  }
113
+ '/_authenticated/consent': {
114
+ id: '/_authenticated/consent'
115
+ path: '/consent'
116
+ fullPath: '/consent'
117
+ preLoaderRoute: typeof AuthenticatedConsentRouteImport
118
+ parentRoute: typeof AuthenticatedRoute
119
+ }
103
120
  }
104
121
  }
105
122
 
106
123
  interface AuthenticatedRouteChildren {
124
+ AuthenticatedConsentRoute: typeof AuthenticatedConsentRoute
107
125
  AuthenticatedIndexRoute: typeof AuthenticatedIndexRoute
108
126
  }
109
127
 
110
128
  const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
129
+ AuthenticatedConsentRoute: AuthenticatedConsentRoute,
111
130
  AuthenticatedIndexRoute: AuthenticatedIndexRoute,
112
131
  }
113
132
 
@@ -0,0 +1,21 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { z } from 'zod'
3
+ import { ConsentForm } from '@/features/oauth/consent-form'
4
+
5
+ // Loose, because every parameter Better Auth signed has to stay in the URL for the consent request.
6
+ export const Route = createFileRoute('/_authenticated/consent')({
7
+ validateSearch: z.looseObject({ client_id: z.string(), scope: z.string().default('') }),
8
+ component: ConsentPage,
9
+ })
10
+
11
+ function ConsentPage() {
12
+ const { auth } = Route.useRouteContext()
13
+ const { client_id, scope } = Route.useSearch()
14
+
15
+ return (
16
+ <>
17
+ <h1 className="text-2xl font-semibold">Allow access</h1>
18
+ <ConsentForm auth={auth} clientId={client_id} scopes={scope.split(' ').filter(Boolean)} />
19
+ </>
20
+ )
21
+ }
@@ -1,9 +1,9 @@
1
+ import { fakeTransport, json } from '@test/support/fake-transport'
2
+ import { renderApp } from '@test/support/render-app'
1
3
  import { screen } from '@testing-library/react'
2
4
  import userEvent from '@testing-library/user-event'
3
5
  import { describe, expect, it } from 'vitest'
4
6
  import type { Profile } from '@/api-contract'
5
- import { fakeTransport, json } from '../test/fake-transport'
6
- import { renderApp } from '../test/render-app'
7
7
 
8
8
  const user = { id: 'user-1', email: 'ana@example.com', name: 'Ana' }
9
9
 
@@ -1,8 +1,8 @@
1
+ import { fakeTransport, json } from '@test/support/fake-transport'
2
+ import { renderApp } from '@test/support/render-app'
1
3
  import { screen, within } from '@testing-library/react'
2
4
  import userEvent from '@testing-library/user-event'
3
5
  import { describe, expect, it } from 'vitest'
4
- import { fakeTransport, json } from '../../../test/fake-transport'
5
- import { renderApp } from '../../../test/render-app'
6
6
 
7
7
  describe('SignInForm', () => {
8
8
  it('shows a rejected sign-in as a form-level error', async () => {
@@ -0,0 +1,45 @@
1
+ import { fakeTransport, json } from '@test/support/fake-transport'
2
+ import { renderApp } from '@test/support/render-app'
3
+ import { screen } from '@testing-library/react'
4
+ import userEvent from '@testing-library/user-event'
5
+ import { afterEach, describe, expect, it } from 'vitest'
6
+
7
+ const user = { id: 'user-1', email: 'ana@example.com', name: 'Ana' }
8
+
9
+ // The shape Better Auth signs: every signed name listed in ba_param, then the signature.
10
+ const SIGNED =
11
+ 'client_id=client-1&scope=openid+profile&ba_param=ba_param&ba_param=client_id&ba_param=scope&sig=signature'
12
+
13
+ afterEach(() => {
14
+ window.history.replaceState(null, '', '/')
15
+ })
16
+
17
+ describe('ConsentForm', () => {
18
+ it('names the client, lists the scopes and sends the signed query with the answer', async () => {
19
+ const visitor = userEvent.setup()
20
+ let consent: Record<string, unknown> | undefined
21
+
22
+ // The OAuth client plugin reads the browser's own URL, not the router's memory history.
23
+ window.history.replaceState(null, '', `/consent?${SIGNED}&unrelated=1`)
24
+
25
+ renderApp(
26
+ `/consent?${SIGNED}`,
27
+ fakeTransport({
28
+ 'GET /api/auth/get-session': () =>
29
+ json(200, { session: { id: 's-1', userId: user.id }, user }),
30
+ 'GET /api/auth/oauth2/public-client': () => json(200, { client_name: 'Claude' }),
31
+ 'POST /api/auth/oauth2/consent': async (request) => {
32
+ consent = await request.json()
33
+ return json(200, { redirect: false })
34
+ },
35
+ }),
36
+ )
37
+
38
+ expect(await screen.findByText('Claude')).toBeInTheDocument()
39
+ expect(screen.getByText('profile')).toBeInTheDocument()
40
+
41
+ await visitor.click(screen.getByRole('button', { name: 'Allow' }))
42
+
43
+ expect(consent).toMatchObject({ accept: true, oauth_query: SIGNED })
44
+ })
45
+ })
@@ -1,10 +1,10 @@
1
1
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
+ import { API_URL, fakeTransport, json } from '@test/support/fake-transport'
2
3
  import { render, screen } from '@testing-library/react'
3
4
  import userEvent from '@testing-library/user-event'
4
5
  import { describe, expect, it } from 'vitest'
5
6
  import { createClient, type Profile } from '@/api-contract'
6
- import { API_URL, fakeTransport, json } from '../../../test/fake-transport'
7
- import { ProfileForm } from './profile-form'
7
+ import { ProfileForm } from '@/features/profile/profile-form'
8
8
 
9
9
  const profile: Profile = {
10
10
  id: '01a09ffe-635c-77e5-9aeb-d3e9d1a7ee24',
@@ -1,10 +1,10 @@
1
1
  import { QueryClient } from '@tanstack/react-query'
2
2
  import { createMemoryHistory } from '@tanstack/react-router'
3
+ import { API_URL } from '@test/support/fake-transport'
3
4
  import { render } from '@testing-library/react'
4
5
  import { createClient, type Transport } from '@/api-contract'
5
6
  import { App } from '@/app'
6
7
  import { createAuth } from '@/features/auth/auth-client'
7
- import { API_URL } from './fake-transport'
8
8
 
9
9
  export function renderApp(path: string, transport: Transport) {
10
10
  const context = {
@@ -13,7 +13,8 @@
13
13
  "isolatedModules": true,
14
14
  "noEmit": true,
15
15
  "paths": {
16
- "@/*": ["./src/*"]
16
+ "@/*": ["./src/*"],
17
+ "@test/*": ["./test/*"]
17
18
  },
18
19
  "types": ["vite/client", "vitest/globals", "@testing-library/jest-dom/vitest"]
19
20
  },
@@ -6,13 +6,13 @@ import { defineConfig } from 'vitest/config'
6
6
  export default defineConfig({
7
7
  plugins: [tanstackRouter({ target: 'react', autoCodeSplitting: true }), react(), tailwindcss()],
8
8
  resolve: {
9
- alias: { '@': `${import.meta.dirname}/src` },
9
+ alias: { '@': `${import.meta.dirname}/src`, '@test': `${import.meta.dirname}/test` },
10
10
  },
11
11
  test: {
12
12
  environment: 'jsdom',
13
13
  globals: true,
14
- include: ['src/**/*.spec.{ts,tsx}'],
15
- setupFiles: ['./test/setup.ts'],
14
+ include: ['test/**/*.spec.{ts,tsx}'],
15
+ setupFiles: ['./test/support/setup.ts'],
16
16
  coverage: { provider: 'v8', include: ['src/**'] },
17
17
  },
18
18
  })
@@ -22,9 +22,13 @@ Node 22.17 or later and pnpm. For the API, Docker Desktop running: its database
22
22
 
23
23
  ## Trying it
24
24
 
25
- Run `pnpm dev`. If the workspace holds `api` and its database is not set up yet, it offers to create one first. If the workspace holds `web`, open `http://localhost:5173`: you are sent to
26
- sign in, and **Sign up** creates an account and takes you to your profile. Each app's README says what it needs
27
- before it starts.
25
+ Run `pnpm dev`. It first checks that every app's port is free, offering to stop whatever holds one or to move that
26
+ app to another port. Ctrl+C stops every app it started, and the ports it used are free again.
27
+
28
+ If the workspace holds `api` and its database is not set up yet, `pnpm dev` offers to create the database first.
29
+
30
+ If the workspace holds `web`, open `http://localhost:5173`: you are sent to sign in, and **Sign up** creates an
31
+ account and takes you to your profile. Each app's README says what it needs before it starts.
28
32
 
29
33
  ## Conventions
30
34