@stjoseph/prumo 0.2.0 → 0.3.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 (163) hide show
  1. package/.prumo-templates/api/auth.md +50 -60
  2. package/.prumo-templates/api/config.md +2 -2
  3. package/.prumo-templates/api/routes.md +6 -4
  4. package/.prumo-templates/api/structure.md +4 -1
  5. package/.prumo-templates/client/data.md +16 -18
  6. package/.prumo-templates/client/forms.md +2 -2
  7. package/.prumo-templates/core/testing.md +4 -5
  8. package/.prumo-templates/database/testing.md +6 -8
  9. package/.prumo-templates/mcp/server.md +39 -12
  10. package/.prumo-templates/mobile/routing.md +1 -1
  11. package/.prumo-templates/mobile/storage.md +7 -6
  12. package/.prumo-templates/monorepo/layout.md +4 -4
  13. package/.prumo-templates/multi-tenancy/client.md +6 -6
  14. package/.prumo-templates/multi-tenancy/isolation.md +17 -10
  15. package/README.md +2 -2
  16. package/dist/clean.js +2 -2
  17. package/dist/compose.js +1 -9
  18. package/dist/generate.js +3 -4
  19. package/dist/mcp.js +15 -8
  20. package/package.json +1 -1
  21. package/templates/api/.env.example +3 -5
  22. package/templates/api/README.md +4 -5
  23. package/templates/api/docker-compose.yml +0 -1
  24. package/templates/api/migrations/Migration20261002210629_create_user_profile_session.ts +31 -0
  25. package/templates/api/migrations/Migration20261002210631_create_authorization.ts +18 -0
  26. package/templates/api/package.json +6 -8
  27. package/templates/api/scripts/database.mjs +3 -11
  28. package/templates/api/scripts/ports.mjs +1 -1
  29. package/templates/api/src/domain/auth/dto/refresh.dto.ts +9 -0
  30. package/templates/api/src/domain/auth/dto/session-response.dto.ts +7 -0
  31. package/templates/api/src/domain/auth/dto/sign-in.dto.ts +8 -0
  32. package/templates/api/src/domain/auth/dto/sign-up.dto.ts +9 -0
  33. package/templates/api/src/domain/auth/dto/tokens-response.dto.ts +10 -0
  34. package/templates/api/src/domain/auth/entities/session.entity.ts +76 -0
  35. package/templates/api/src/domain/auth/entities/user.entity.ts +17 -0
  36. package/templates/api/src/domain/auth/errors/email-taken.error.ts +9 -0
  37. package/templates/api/src/domain/auth/errors/invalid-credentials.error.ts +10 -0
  38. package/templates/api/src/domain/auth/errors/invalid-session.error.ts +9 -0
  39. package/templates/api/src/domain/auth/errors/superseded-token.error.ts +11 -0
  40. package/templates/api/src/domain/auth/ports/access-tokens.port.ts +15 -0
  41. package/templates/api/src/domain/auth/ports/opaque-tokens.port.ts +7 -0
  42. package/templates/api/src/domain/auth/ports/password-hasher.port.ts +8 -0
  43. package/templates/api/src/domain/auth/repositories/session.repository.ts +10 -0
  44. package/templates/api/src/domain/auth/repositories/user.repository.ts +9 -0
  45. package/templates/api/src/domain/auth/use-cases/find-user.use-case.ts +20 -0
  46. package/templates/api/src/domain/auth/use-cases/refresh-session.use-case.ts +92 -0
  47. package/templates/api/src/domain/auth/use-cases/sign-in.use-case.ts +30 -0
  48. package/templates/api/src/domain/auth/use-cases/sign-out.use-case.ts +22 -0
  49. package/templates/api/src/domain/auth/use-cases/sign-up.use-case.ts +46 -0
  50. package/templates/api/src/domain/auth/use-cases/start-session.use-case.ts +48 -0
  51. package/templates/api/src/domain/oauth/dto/authorization-decision.dto.ts +7 -0
  52. package/templates/api/src/domain/oauth/dto/authorization-view.dto.ts +13 -0
  53. package/templates/api/src/domain/oauth/entities/authorization.entity.ts +83 -0
  54. package/templates/api/src/domain/oauth/errors/authorization-not-found.error.ts +9 -0
  55. package/templates/api/src/domain/oauth/errors/authorization-request.error.ts +14 -0
  56. package/templates/api/src/domain/oauth/errors/invalid-client.error.ts +6 -0
  57. package/templates/api/src/domain/oauth/errors/invalid-grant.error.ts +6 -0
  58. package/templates/api/src/domain/oauth/ports/authorization-codes.port.ts +8 -0
  59. package/templates/api/src/domain/oauth/ports/client-metadata.port.ts +9 -0
  60. package/templates/api/src/domain/oauth/repositories/authorization.repository.ts +10 -0
  61. package/templates/api/src/domain/oauth/use-cases/decide-authorization.use-case.ts +53 -0
  62. package/templates/api/src/domain/oauth/use-cases/exchange-code.use-case.ts +96 -0
  63. package/templates/api/src/domain/oauth/use-cases/find-authorization.use-case.ts +24 -0
  64. package/templates/api/src/domain/oauth/use-cases/start-authorization.use-case.ts +75 -0
  65. package/templates/api/src/infra/auth/argon2-password-hasher.adapter.ts +21 -0
  66. package/templates/api/src/infra/auth/crypto-opaque-tokens.adapter.ts +14 -0
  67. package/templates/api/src/infra/auth/jose-access-tokens.adapter.ts +62 -0
  68. package/templates/api/src/infra/config/env.ts +3 -7
  69. package/templates/api/src/infra/database/mikroorm/entities/authorization.schema.ts +37 -0
  70. package/templates/api/src/infra/database/mikroorm/entities/profile.schema.ts +1 -1
  71. package/templates/api/src/infra/database/mikroorm/entities/session.schema.ts +33 -0
  72. package/templates/api/src/infra/database/mikroorm/entities/user.schema.ts +18 -0
  73. package/templates/api/src/infra/database/mikroorm/mikro-orm.factory.ts +9 -4
  74. package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-authorization.repository.ts +21 -0
  75. package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-session.repository.ts +21 -0
  76. package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-user.repository.ts +31 -0
  77. package/templates/api/src/infra/di/auth.di.ts +21 -0
  78. package/templates/api/src/infra/di/index.ts +4 -10
  79. package/templates/api/src/infra/di/oauth.di.ts +14 -0
  80. package/templates/api/src/infra/http/app.ts +11 -1
  81. package/templates/api/src/infra/http/controllers/auth.controller.ts +153 -30
  82. package/templates/api/src/infra/http/controllers/mcp.controller.ts +32 -27
  83. package/templates/api/src/infra/http/controllers/oauth.controller.ts +257 -0
  84. package/templates/api/src/infra/http/errors/error-handler.ts +1 -0
  85. package/templates/api/src/infra/http/fastify.d.ts +2 -3
  86. package/templates/api/src/infra/http/fetch-bridge.ts +17 -5
  87. package/templates/api/src/infra/http/hooks/auth.hook.ts +25 -8
  88. package/templates/api/src/infra/http/hooks/origin.hook.ts +22 -0
  89. package/templates/api/src/infra/http/server.ts +1 -4
  90. package/templates/api/src/infra/mcp/mcp.server.ts +10 -0
  91. package/templates/api/src/infra/oauth/crypto-authorization-codes.adapter.ts +21 -0
  92. package/templates/api/src/infra/oauth/https-client-metadata.adapter.ts +231 -0
  93. package/templates/api/test/domain/auth/entities/session.entity.spec.ts +50 -0
  94. package/templates/api/test/domain/auth/use-cases/refresh-session.use-case.spec.ts +66 -0
  95. package/templates/api/test/domain/auth/use-cases/sign-in.use-case.spec.ts +29 -0
  96. package/templates/api/test/domain/auth/use-cases/sign-up.use-case.spec.ts +33 -0
  97. package/templates/api/test/domain/oauth/entities/authorization.entity.spec.ts +52 -0
  98. package/templates/api/test/infra/auth/argon2-password-hasher.adapter.spec.ts +17 -0
  99. package/templates/api/test/infra/auth/jose-access-tokens.adapter.spec.ts +57 -0
  100. package/templates/api/test/infra/database/mikroorm/mikro-orm.factory.spec.ts +1 -1
  101. package/templates/api/test/infra/database/mikroorm/repositories/mikroorm-user.repository.spec.ts +28 -0
  102. package/templates/api/test/infra/http/controllers/auth.controller.spec.ts +201 -0
  103. package/templates/api/test/infra/http/controllers/mcp.controller.spec.ts +232 -54
  104. package/templates/api/test/infra/http/controllers/users.controller.spec.ts +3 -3
  105. package/templates/api/test/infra/oauth/crypto-authorization-codes.adapter.spec.ts +18 -0
  106. package/templates/api/test/infra/oauth/https-client-metadata.adapter.spec.ts +58 -0
  107. package/templates/api/test/support/factories/user.factory.ts +9 -11
  108. package/templates/api/test/support/fakes/auth-fakes.ts +42 -0
  109. package/templates/api/test/support/fakes/fake-access-tokens.ts +19 -0
  110. package/templates/api/test/support/fakes/fake-opaque-tokens.ts +14 -0
  111. package/templates/api/test/support/fakes/fake-password-hasher.ts +15 -0
  112. package/templates/api/test/support/fakes/immediate-transaction-manager.ts +8 -0
  113. package/templates/api/test/support/fakes/in-memory-session.repository.ts +19 -0
  114. package/templates/api/test/support/fakes/in-memory-user.repository.ts +20 -0
  115. package/templates/api/test/support/setup.ts +6 -28
  116. package/templates/mobile/README.md +1 -1
  117. package/templates/mobile/jest.config.js +0 -17
  118. package/templates/mobile/package.json +0 -4
  119. package/templates/mobile/scripts/ports.mjs +1 -1
  120. package/templates/mobile/src/api-contract/api-error.ts +0 -11
  121. package/templates/mobile/src/api-contract/auth.ts +161 -0
  122. package/templates/mobile/src/api-contract/index.ts +10 -0
  123. package/templates/mobile/src/app/(app)/_layout.tsx +1 -1
  124. package/templates/mobile/src/app/_layout.tsx +5 -5
  125. package/templates/mobile/src/features/auth/sign-in-form.tsx +12 -8
  126. package/templates/mobile/src/features/auth/sign-out-button.tsx +4 -4
  127. package/templates/mobile/src/features/auth/sign-up-form.tsx +13 -9
  128. package/templates/mobile/src/features/auth/token-store.ts +21 -0
  129. package/templates/mobile/src/features/clients/clients-context.tsx +1 -2
  130. package/templates/mobile/test/features/auth/authenticated-layout.spec.tsx +16 -14
  131. package/templates/mobile/test/features/auth/sign-in-form.spec.tsx +12 -11
  132. package/templates/mobile/test/features/auth/sign-out-button.spec.tsx +8 -9
  133. package/templates/mobile/test/support/memory-token-store.ts +15 -0
  134. package/templates/site/scripts/ports.mjs +1 -1
  135. package/templates/web/package.json +0 -3
  136. package/templates/web/scripts/ports.mjs +1 -1
  137. package/templates/web/src/api-contract/api-error.ts +0 -11
  138. package/templates/web/src/api-contract/auth.ts +161 -0
  139. package/templates/web/src/api-contract/index.ts +10 -0
  140. package/templates/web/src/features/auth/sign-in-form.tsx +12 -15
  141. package/templates/web/src/features/auth/sign-out-button.tsx +4 -4
  142. package/templates/web/src/features/auth/sign-up-form.tsx +7 -9
  143. package/templates/web/src/features/oauth/consent-form.tsx +66 -35
  144. package/templates/web/src/main.tsx +6 -4
  145. package/templates/web/src/router.tsx +1 -2
  146. package/templates/web/src/routes/_authenticated/consent.tsx +5 -4
  147. package/templates/web/src/routes/_authenticated.tsx +1 -1
  148. package/templates/web/test/api-contract/auth.spec.ts +155 -0
  149. package/templates/web/test/app.spec.tsx +47 -22
  150. package/templates/web/test/features/auth/sign-in-form.spec.tsx +6 -2
  151. package/templates/web/test/features/oauth/consent-form.spec.tsx +83 -26
  152. package/templates/web/test/support/render-app.tsx +4 -4
  153. package/templates/workspace/scripts/ports.mjs +1 -1
  154. package/templates/api/docker/init/01-auth-schema.sql +0 -1
  155. package/templates/api/migrations/Migration20260914123239_create_profile.ts +0 -13
  156. package/templates/api/src/infra/auth/auth.cli.ts +0 -5
  157. package/templates/api/src/infra/auth/auth.factory.ts +0 -77
  158. package/templates/api/src/infra/database/mikroorm/entities/auth-user.schema.ts +0 -16
  159. package/templates/api/test/infra/auth/auth.factory.spec.ts +0 -22
  160. package/templates/mobile/src/features/auth/auth-client.ts +0 -27
  161. package/templates/mobile/src/features/auth/session.ts +0 -17
  162. package/templates/web/src/features/auth/auth-client.ts +0 -17
  163. package/templates/web/src/features/auth/session.ts +0 -17
@@ -25,7 +25,7 @@ const APPS = {
25
25
  // A port that moves takes these with it: the key to write, in which app, from which app's port.
26
26
  const FOLLOWERS = {
27
27
  api: [
28
- { app: 'api', key: 'BETTER_AUTH_URL' },
28
+ { app: 'api', key: 'API_URL' },
29
29
  { app: 'web', key: 'VITE_API_URL' },
30
30
  { app: 'mobile', key: 'EXPO_PUBLIC_API_URL' },
31
31
  ],
@@ -1 +0,0 @@
1
- CREATE SCHEMA IF NOT EXISTS auth;
@@ -1,13 +0,0 @@
1
- import { Migration } from '@mikro-orm/migrations';
2
-
3
- export class Migration20260914123239_create_profile extends Migration {
4
-
5
- override name = 'Migration20260914123239_create_profile';
6
-
7
- override up(): void | Promise<void> {
8
- this.addSql(`create table "profile" ("id" uuid not null default uuidv7(), "user_id" uuid not null, "display_name" text not null, "locale" text not null default 'en', "timezone" text not null default 'UTC', "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), primary key ("id"));`);
9
- this.addSql(`alter table "profile" add constraint "profile_user_id_unique" unique ("user_id");`);
10
- this.addSql(`alter table "profile" add constraint "profile_user_id_foreign" foreign key ("user_id") references "auth"."user" ("id") on delete restrict;`);
11
- }
12
-
13
- }
@@ -1,5 +0,0 @@
1
- import { loadEnv } from '@/infra/config/env'
2
- import { createAuth } from './auth.factory'
3
-
4
- // Read by Better Auth's CLI (`auth migrate --config`), which only needs the schema, not the hooks.
5
- export const auth = createAuth(loadEnv())
@@ -1,77 +0,0 @@
1
- import { cimd } from '@better-auth/cimd' // prumo:mcp
2
- import { fetchClientMetadataResource } from '@better-auth/cimd/node' // prumo:mcp
3
- import { expo } from '@better-auth/expo'
4
- import { mcp } from '@better-auth/mcp' // prumo:mcp
5
- import { type BetterAuthPlugin, betterAuth } from 'better-auth'
6
- import { jwt } from 'better-auth/plugins' // prumo:mcp
7
- import { Pool } from 'pg'
8
- import type { Env } from '@/infra/config/env'
9
-
10
- type NewUser = { id: string; name: string }
11
-
12
- export type AuthHooks = {
13
- onUserCreated?: (user: NewUser) => Promise<void>
14
- }
15
-
16
- export const AUTH = Symbol('Auth')
17
-
18
- // prumo:mcp
19
- // The protected resource MCP clients ask a token for; access tokens carry it as their audience.
20
- export function mcpResource(env: Pick<Env, 'BETTER_AUTH_URL'>): string {
21
- return `${env.BETTER_AUTH_URL}/api/mcp`
22
- }
23
-
24
- // prumo:end-mcp
25
- export function createAuth(
26
- env: Pick<
27
- Env,
28
- | 'AUTH_DATABASE_URL'
29
- | 'BETTER_AUTH_SECRET'
30
- | 'BETTER_AUTH_URL'
31
- | 'WEB_ORIGIN'
32
- | 'MOBILE_APP_SCHEME'
33
- >,
34
- hooks: AuthHooks = {},
35
- ) {
36
- const mobile = env.MOBILE_APP_SCHEME
37
- const plugins: BetterAuthPlugin[] = mobile === undefined ? [] : [expo()]
38
-
39
- // prumo:mcp
40
- plugins.push(
41
- jwt(),
42
- mcp({
43
- loginPage: `${env.WEB_ORIGIN}/sign-in`,
44
- consentPage: `${env.WEB_ORIGIN}/consent`,
45
- resource: mcpResource(env),
46
- }),
47
- cimd({ fetchClientMetadataResource, metadataProfile: 'mcp-2026-07-28' }),
48
- )
49
-
50
- // prumo:end-mcp
51
- return betterAuth({
52
- baseURL: env.BETTER_AUTH_URL,
53
- basePath: '/api/auth',
54
- secret: env.BETTER_AUTH_SECRET,
55
- trustedOrigins: mobile === undefined ? [env.WEB_ORIGIN] : [env.WEB_ORIGIN, `${mobile}://`],
56
- plugins,
57
- database: new Pool({
58
- connectionString: env.AUTH_DATABASE_URL,
59
- options: '-c search_path=auth',
60
- }),
61
- emailAndPassword: { enabled: true },
62
- advanced: { database: { generateId: 'uuid' } },
63
- databaseHooks: {
64
- user: {
65
- create: {
66
- after: async (user) => {
67
- await hooks.onUserCreated?.(user)
68
- },
69
- },
70
- },
71
- },
72
- })
73
- }
74
-
75
- export type Auth = ReturnType<typeof createAuth>
76
- export type AuthSession = Awaited<ReturnType<Auth['api']['getSession']>>
77
- export type AuthUser = NonNullable<AuthSession>['user']
@@ -1,16 +0,0 @@
1
- import { EntitySchema } from '@mikro-orm/core'
2
-
3
- // Better Auth owns auth.user. This maps only its id, so a foreign key into it is declared rather than
4
- // hand-written; schema diffing skips the table, and nothing reads or writes it through the ORM.
5
- export class AuthUser {
6
- id: string
7
- }
8
-
9
- export const AuthUserSchema = new EntitySchema({
10
- class: AuthUser,
11
- schema: 'auth',
12
- tableName: 'user',
13
- properties: {
14
- id: { type: 'uuid', primary: true },
15
- },
16
- })
@@ -1,22 +0,0 @@
1
- import { testEnv } from '@test/support/setup'
2
- import { describe, expect, it } from 'vitest'
3
- import { createAuth } from '@/infra/auth/auth.factory'
4
-
5
- const proxy =
6
- 'http://localhost:3000/api/auth/expo-authorization-proxy?authorizationURL=https://example.com/authorize&oauthState=s'
7
-
8
- describe('createAuth', () => {
9
- it('exposes no Expo redirect endpoint without a mobile scheme', async () => {
10
- const response = await createAuth(testEnv()).handler(new Request(proxy))
11
-
12
- expect(response.status).toBe(404)
13
- })
14
-
15
- it('exposes the Expo endpoint and trusts the scheme once a mobile scheme is set', async () => {
16
- const auth = createAuth({ ...testEnv(), MOBILE_APP_SCHEME: 'app' })
17
- const response = await auth.handler(new Request(proxy))
18
-
19
- expect(response.status).toBe(302)
20
- expect(auth.options.trustedOrigins).toContain('app://')
21
- })
22
- })
@@ -1,27 +0,0 @@
1
- import { expoClient } from '@better-auth/expo/client'
2
- import { createAuthClient } from 'better-auth/react'
3
- import * as SecureStore from 'expo-secure-store'
4
- import type { Transport } from '@/api-contract'
5
-
6
- export type AuthClient = ReturnType<typeof createAuth>
7
-
8
- export type SessionAuth = Pick<AuthClient, 'signIn' | 'signUp' | 'getSession'>
9
-
10
- export function createAuth({ baseUrl, fetch }: { baseUrl: string; fetch: Transport }) {
11
- return createAuthClient({
12
- baseURL: baseUrl,
13
- basePath: '/api/auth',
14
- fetchOptions: { customFetchImpl: fetch },
15
- plugins: [expoClient({ storage: SecureStore })],
16
- })
17
- }
18
-
19
- export function withSessionCookie(auth: AuthClient, fetch: Transport): Transport {
20
- return async (input, init) => {
21
- const headers = new Headers(init?.headers)
22
-
23
- headers.set('cookie', await auth.getCookie())
24
-
25
- return fetch(input, { ...init, headers, credentials: 'omit' })
26
- }
27
- }
@@ -1,17 +0,0 @@
1
- import { queryOptions } from '@tanstack/react-query'
2
- import { ApiError } from '@/api-contract'
3
- import type { SessionAuth } from './auth-client'
4
-
5
- export const sessionQuery = (auth: SessionAuth) =>
6
- queryOptions({
7
- queryKey: ['session'],
8
- queryFn: async () => {
9
- const { data, error } = await auth.getSession()
10
-
11
- if (error !== null) {
12
- throw ApiError.fromAuthError(error)
13
- }
14
-
15
- return data
16
- },
17
- })
@@ -1,17 +0,0 @@
1
- import { oauthProviderClient } from '@better-auth/oauth-provider/client' // prumo:mcp
2
- import { createAuthClient } from 'better-auth/react'
3
- import type { Transport } from '@/api-contract'
4
-
5
- export type AuthClient = ReturnType<typeof createAuth>
6
-
7
- export function createAuth({ baseUrl, fetch }: { baseUrl: string; fetch: Transport }) {
8
- return createAuthClient({
9
- baseURL: baseUrl,
10
- basePath: '/api/auth',
11
- fetchOptions: { customFetchImpl: fetch },
12
- // prumo:mcp
13
- // Carries the signed query of an MCP client's authorization through sign-in and consent.
14
- plugins: [oauthProviderClient()],
15
- // prumo:end-mcp
16
- })
17
- }
@@ -1,17 +0,0 @@
1
- import { queryOptions } from '@tanstack/react-query'
2
- import { ApiError } from '@/api-contract'
3
- import type { AuthClient } from './auth-client'
4
-
5
- export const sessionQuery = (auth: AuthClient) =>
6
- queryOptions({
7
- queryKey: ['session'],
8
- queryFn: async () => {
9
- const { data, error } = await auth.getSession()
10
-
11
- if (error !== null) {
12
- throw ApiError.fromAuthError(error)
13
- }
14
-
15
- return data
16
- },
17
- })