@stjoseph/prumo 0.2.0 → 0.4.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.
- package/.prumo-templates/api/auth.md +51 -60
- package/.prumo-templates/api/config.md +2 -2
- package/.prumo-templates/api/routes.md +6 -4
- package/.prumo-templates/api/structure.md +4 -1
- package/.prumo-templates/client/data.md +16 -18
- package/.prumo-templates/client/forms.md +2 -2
- package/.prumo-templates/core/testing.md +4 -4
- package/.prumo-templates/database/testing.md +6 -8
- package/.prumo-templates/email/codes.md +62 -0
- package/.prumo-templates/email/mailer.md +47 -0
- package/.prumo-templates/mcp/server.md +55 -14
- package/.prumo-templates/mobile/routing.md +1 -1
- package/.prumo-templates/mobile/storage.md +7 -6
- package/.prumo-templates/monorepo/layout.md +4 -4
- package/.prumo-templates/multi-tenancy/client.md +6 -6
- package/.prumo-templates/multi-tenancy/isolation.md +17 -10
- package/.prumo-templates/social/sign-in.md +77 -0
- package/README.md +8 -6
- package/dist/asker.js +13 -0
- package/dist/clean.js +2 -2
- package/dist/cli.js +16 -2
- package/dist/commands.js +7 -0
- package/dist/compose.js +3 -11
- package/dist/context.js +4 -0
- package/dist/features.js +230 -0
- package/dist/generate.js +20 -7
- package/dist/questions.js +64 -1
- package/package.json +1 -1
- package/templates/api/.env.example +21 -5
- package/templates/api/README.md +4 -5
- package/templates/api/docker-compose.yml +0 -1
- package/templates/api/migrations/Migration20261005111545_create_user_profile_session.ts +31 -0
- package/templates/api/migrations/Migration20261005111547_create_authorization.ts +18 -0
- package/templates/api/migrations/Migration20261005111549_create_email_code.ts +18 -0
- package/templates/api/migrations/Migration20261005111550_create_identity_social_sign_in.ts +25 -0
- package/templates/api/package.json +7 -8
- package/templates/api/scripts/database.mjs +3 -11
- package/templates/api/scripts/ports.mjs +1 -1
- package/templates/api/src/domain/auth/dto/email-request.dto.ts +5 -0
- package/templates/api/src/domain/auth/dto/refresh.dto.ts +9 -0
- package/templates/api/src/domain/auth/dto/reset-password.dto.ts +9 -0
- package/templates/api/src/domain/auth/dto/session-response.dto.ts +7 -0
- package/templates/api/src/domain/auth/dto/sign-in.dto.ts +8 -0
- package/templates/api/src/domain/auth/dto/sign-up.dto.ts +9 -0
- package/templates/api/src/domain/auth/dto/tokens-response.dto.ts +10 -0
- package/templates/api/src/domain/auth/dto/verify-email.dto.ts +8 -0
- package/templates/api/src/domain/auth/entities/email-code.entity.ts +46 -0
- package/templates/api/src/domain/auth/entities/identity.entity.ts +18 -0
- package/templates/api/src/domain/auth/entities/session.entity.ts +78 -0
- package/templates/api/src/domain/auth/entities/social-sign-in.entity.ts +82 -0
- package/templates/api/src/domain/auth/entities/user.entity.ts +34 -0
- package/templates/api/src/domain/auth/errors/email-not-verified.error.ts +10 -0
- package/templates/api/src/domain/auth/errors/email-taken.error.ts +9 -0
- package/templates/api/src/domain/auth/errors/invalid-code.error.ts +11 -0
- package/templates/api/src/domain/auth/errors/invalid-credentials.error.ts +10 -0
- package/templates/api/src/domain/auth/errors/invalid-session.error.ts +9 -0
- package/templates/api/src/domain/auth/errors/provider-email-unverified.error.ts +11 -0
- package/templates/api/src/domain/auth/errors/provider-not-configured.error.ts +10 -0
- package/templates/api/src/domain/auth/errors/social-sign-in-failed.error.ts +11 -0
- package/templates/api/src/domain/auth/errors/superseded-token.error.ts +11 -0
- package/templates/api/src/domain/auth/ports/access-tokens.port.ts +15 -0
- package/templates/api/src/domain/auth/ports/identity-providers.port.ts +29 -0
- package/templates/api/src/domain/auth/ports/mailer.port.ts +8 -0
- package/templates/api/src/domain/auth/ports/one-time-codes.port.ts +7 -0
- package/templates/api/src/domain/auth/ports/opaque-tokens.port.ts +7 -0
- package/templates/api/src/domain/auth/ports/password-hasher.port.ts +8 -0
- package/templates/api/src/domain/auth/repositories/email-code.repository.ts +9 -0
- package/templates/api/src/domain/auth/repositories/identity.repository.ts +9 -0
- package/templates/api/src/domain/auth/repositories/session.repository.ts +12 -0
- package/templates/api/src/domain/auth/repositories/social-sign-in.repository.ts +9 -0
- package/templates/api/src/domain/auth/repositories/user.repository.ts +9 -0
- package/templates/api/src/domain/auth/use-cases/complete-social-sign-in.use-case.ts +103 -0
- package/templates/api/src/domain/auth/use-cases/exchange-social-code.use-case.ts +56 -0
- package/templates/api/src/domain/auth/use-cases/find-user.use-case.ts +20 -0
- package/templates/api/src/domain/auth/use-cases/issue-email-code.use-case.ts +55 -0
- package/templates/api/src/domain/auth/use-cases/link-identity.use-case.ts +77 -0
- package/templates/api/src/domain/auth/use-cases/refresh-session.use-case.ts +94 -0
- package/templates/api/src/domain/auth/use-cases/request-email-verification.use-case.ts +21 -0
- package/templates/api/src/domain/auth/use-cases/request-password-reset.use-case.ts +21 -0
- package/templates/api/src/domain/auth/use-cases/reset-password.use-case.ts +73 -0
- package/templates/api/src/domain/auth/use-cases/sign-in.use-case.ts +38 -0
- package/templates/api/src/domain/auth/use-cases/sign-out.use-case.ts +22 -0
- package/templates/api/src/domain/auth/use-cases/sign-up.use-case.ts +57 -0
- package/templates/api/src/domain/auth/use-cases/start-session.use-case.ts +55 -0
- package/templates/api/src/domain/auth/use-cases/start-social-sign-in.use-case.ts +54 -0
- package/templates/api/src/domain/auth/use-cases/verify-email.use-case.ts +62 -0
- package/templates/api/src/domain/oauth/dto/authorization-decision.dto.ts +7 -0
- package/templates/api/src/domain/oauth/dto/authorization-view.dto.ts +14 -0
- package/templates/api/src/domain/oauth/entities/authorization.entity.ts +87 -0
- package/templates/api/src/domain/oauth/errors/authorization-not-found.error.ts +9 -0
- package/templates/api/src/domain/oauth/errors/authorization-request.error.ts +18 -0
- package/templates/api/src/domain/oauth/errors/invalid-client.error.ts +6 -0
- package/templates/api/src/domain/oauth/errors/invalid-grant.error.ts +6 -0
- package/templates/api/src/domain/oauth/ports/authorization-codes.port.ts +8 -0
- package/templates/api/src/domain/oauth/ports/client-metadata.port.ts +9 -0
- package/templates/api/src/domain/oauth/repositories/authorization.repository.ts +10 -0
- package/templates/api/src/domain/oauth/use-cases/decide-authorization.use-case.ts +53 -0
- package/templates/api/src/domain/oauth/use-cases/exchange-code.use-case.ts +97 -0
- package/templates/api/src/domain/oauth/use-cases/find-authorization.use-case.ts +24 -0
- package/templates/api/src/domain/oauth/use-cases/start-authorization.use-case.ts +89 -0
- package/templates/api/src/domain/shared/errors/domain-error.ts +2 -0
- package/templates/api/src/infra/auth/argon2-password-hasher.adapter.ts +21 -0
- package/templates/api/src/infra/auth/crypto-opaque-tokens.adapter.ts +14 -0
- package/templates/api/src/infra/auth/jose-access-tokens.adapter.ts +64 -0
- package/templates/api/src/infra/config/env.ts +17 -3
- package/templates/api/src/infra/database/mikroorm/entities/authorization.schema.ts +38 -0
- package/templates/api/src/infra/database/mikroorm/entities/email-code.schema.ts +30 -0
- package/templates/api/src/infra/database/mikroorm/entities/identity.schema.ts +29 -0
- package/templates/api/src/infra/database/mikroorm/entities/profile.schema.ts +1 -1
- package/templates/api/src/infra/database/mikroorm/entities/session.schema.ts +34 -0
- package/templates/api/src/infra/database/mikroorm/entities/social-sign-in.schema.ts +35 -0
- package/templates/api/src/infra/database/mikroorm/entities/user.schema.ts +19 -0
- package/templates/api/src/infra/database/mikroorm/mikro-orm.factory.ts +15 -4
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-authorization.repository.ts +21 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-email-code.repository.ts +17 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-identity.repository.ts +18 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-session.repository.ts +25 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-social-sign-in.repository.ts +17 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-user.repository.ts +31 -0
- package/templates/api/src/infra/di/auth.di.ts +21 -0
- package/templates/api/src/infra/di/email.di.ts +23 -0
- package/templates/api/src/infra/di/index.ts +8 -10
- package/templates/api/src/infra/di/oauth.di.ts +14 -0
- package/templates/api/src/infra/di/social.di.ts +17 -0
- package/templates/api/src/infra/email/hmac-one-time-codes.adapter.ts +16 -0
- package/templates/api/src/infra/email/log-mailer.adapter.ts +11 -0
- package/templates/api/src/infra/http/app.ts +15 -1
- package/templates/api/src/infra/http/controllers/auth-email.controller.ts +73 -0
- package/templates/api/src/infra/http/controllers/auth.controller.ts +108 -29
- package/templates/api/src/infra/http/controllers/mcp.controller.ts +46 -27
- package/templates/api/src/infra/http/controllers/oauth.controller.ts +267 -0
- package/templates/api/src/infra/http/controllers/social.controller.ts +192 -0
- package/templates/api/src/infra/http/errors/error-handler.ts +5 -0
- package/templates/api/src/infra/http/fastify.d.ts +2 -3
- package/templates/api/src/infra/http/fetch-bridge.ts +17 -5
- package/templates/api/src/infra/http/hooks/auth.hook.ts +25 -8
- package/templates/api/src/infra/http/hooks/origin.hook.ts +22 -0
- package/templates/api/src/infra/http/server.ts +1 -4
- package/templates/api/src/infra/http/session-delivery.ts +69 -0
- package/templates/api/src/infra/mcp/mcp.scopes.ts +27 -0
- package/templates/api/src/infra/mcp/mcp.server.ts +10 -0
- package/templates/api/src/infra/mcp/tools/users.tool.ts +6 -0
- package/templates/api/src/infra/oauth/crypto-authorization-codes.adapter.ts +21 -0
- package/templates/api/src/infra/oauth/https-client-metadata.adapter.ts +231 -0
- package/templates/api/src/infra/social/openid-identity-providers.adapter.ts +199 -0
- package/templates/api/test/domain/auth/entities/session.entity.spec.ts +50 -0
- package/templates/api/test/domain/auth/use-cases/email.use-case.spec.ts +87 -0
- package/templates/api/test/domain/auth/use-cases/refresh-session.use-case.spec.ts +66 -0
- package/templates/api/test/domain/auth/use-cases/sign-in.use-case.spec.ts +29 -0
- package/templates/api/test/domain/auth/use-cases/sign-up.use-case.spec.ts +41 -0
- package/templates/api/test/domain/auth/use-cases/social.use-case.spec.ts +148 -0
- package/templates/api/test/domain/oauth/entities/authorization.entity.spec.ts +53 -0
- package/templates/api/test/infra/auth/argon2-password-hasher.adapter.spec.ts +17 -0
- package/templates/api/test/infra/auth/jose-access-tokens.adapter.spec.ts +57 -0
- package/templates/api/test/infra/database/mikroorm/mikro-orm.factory.spec.ts +1 -1
- package/templates/api/test/infra/database/mikroorm/repositories/mikroorm-user.repository.spec.ts +28 -0
- package/templates/api/test/infra/email/hmac-one-time-codes.adapter.spec.ts +15 -0
- package/templates/api/test/infra/http/controllers/auth-email.controller.spec.ts +92 -0
- package/templates/api/test/infra/http/controllers/auth.controller.spec.ts +168 -0
- package/templates/api/test/infra/http/controllers/mcp.controller.spec.ts +309 -67
- package/templates/api/test/infra/http/controllers/social.controller.spec.ts +108 -0
- package/templates/api/test/infra/http/controllers/users.controller.spec.ts +13 -35
- package/templates/api/test/infra/oauth/crypto-authorization-codes.adapter.spec.ts +18 -0
- package/templates/api/test/infra/oauth/https-client-metadata.adapter.spec.ts +58 -0
- package/templates/api/test/infra/social/openid-identity-providers.adapter.spec.ts +154 -0
- package/templates/api/test/support/factories/user.factory.ts +9 -11
- package/templates/api/test/support/fake-oidc-server.ts +96 -0
- package/templates/api/test/support/fakes/auth-fakes.ts +94 -0
- package/templates/api/test/support/fakes/fake-access-tokens.ts +19 -0
- package/templates/api/test/support/fakes/fake-mailer.ts +21 -0
- package/templates/api/test/support/fakes/fake-one-time-codes.ts +14 -0
- package/templates/api/test/support/fakes/fake-opaque-tokens.ts +14 -0
- package/templates/api/test/support/fakes/fake-password-hasher.ts +15 -0
- package/templates/api/test/support/fakes/immediate-transaction-manager.ts +8 -0
- package/templates/api/test/support/fakes/in-memory-email-code.repository.ts +15 -0
- package/templates/api/test/support/fakes/in-memory-session.repository.ts +27 -0
- package/templates/api/test/support/fakes/in-memory-user.repository.ts +20 -0
- package/templates/api/test/support/fakes/social-fakes.ts +112 -0
- package/templates/api/test/support/setup.ts +6 -28
- package/templates/api/test/support/test-app.ts +65 -0
- package/templates/mobile/README.md +1 -1
- package/templates/mobile/jest.config.js +0 -17
- package/templates/mobile/package.json +0 -4
- package/templates/mobile/scripts/ports.mjs +1 -1
- package/templates/mobile/src/api-contract/api-error.ts +4 -11
- package/templates/mobile/src/api-contract/auth.ts +221 -0
- package/templates/mobile/src/api-contract/index.ts +14 -0
- package/templates/mobile/src/app/(app)/_layout.tsx +1 -1
- package/templates/mobile/src/app/(auth)/forgot-password.tsx +20 -0
- package/templates/mobile/src/app/(auth)/reset-password.tsx +18 -0
- package/templates/mobile/src/app/(auth)/sign-in.tsx +9 -0
- package/templates/mobile/src/app/(auth)/sign-up.tsx +4 -0
- package/templates/mobile/src/app/(auth)/verify-email.tsx +18 -0
- package/templates/mobile/src/app/_layout.tsx +5 -5
- package/templates/mobile/src/features/auth/forgot-password-form.tsx +67 -0
- package/templates/mobile/src/features/auth/reset-password-form.tsx +93 -0
- package/templates/mobile/src/features/auth/sign-in-form.tsx +21 -7
- package/templates/mobile/src/features/auth/sign-out-button.tsx +4 -4
- package/templates/mobile/src/features/auth/sign-up-form.tsx +24 -8
- package/templates/mobile/src/features/auth/social-buttons.tsx +79 -0
- package/templates/mobile/src/features/auth/token-store.ts +21 -0
- package/templates/mobile/src/features/auth/verify-email-form.tsx +81 -0
- package/templates/mobile/src/features/clients/clients-context.tsx +1 -2
- package/templates/mobile/test/features/auth/authenticated-layout.spec.tsx +16 -14
- package/templates/mobile/test/features/auth/email-flows.spec.tsx +86 -0
- package/templates/mobile/test/features/auth/sign-in-form.spec.tsx +12 -11
- package/templates/mobile/test/features/auth/sign-out-button.spec.tsx +8 -9
- package/templates/mobile/test/features/auth/social-buttons.spec.tsx +77 -0
- package/templates/mobile/test/support/memory-token-store.ts +15 -0
- package/templates/site/scripts/ports.mjs +1 -1
- package/templates/web/package.json +0 -3
- package/templates/web/scripts/ports.mjs +1 -1
- package/templates/web/src/api-contract/api-error.ts +4 -11
- package/templates/web/src/api-contract/auth.ts +221 -0
- package/templates/web/src/api-contract/index.ts +14 -0
- package/templates/web/src/features/auth/forgot-password-form.tsx +52 -0
- package/templates/web/src/features/auth/reset-password-form.tsx +82 -0
- package/templates/web/src/features/auth/sign-in-form.tsx +24 -14
- package/templates/web/src/features/auth/sign-out-button.tsx +4 -4
- package/templates/web/src/features/auth/sign-up-form.tsx +16 -8
- package/templates/web/src/features/auth/social-buttons.tsx +45 -0
- package/templates/web/src/features/auth/verify-email-form.tsx +84 -0
- package/templates/web/src/features/oauth/consent-form.tsx +72 -35
- package/templates/web/src/main.tsx +6 -4
- package/templates/web/src/routeTree.gen.ts +73 -2
- package/templates/web/src/router.tsx +1 -2
- package/templates/web/src/routes/_authenticated/consent.tsx +5 -4
- package/templates/web/src/routes/_authenticated.tsx +1 -1
- package/templates/web/src/routes/forgot-password.tsx +17 -0
- package/templates/web/src/routes/reset-password.tsx +20 -0
- package/templates/web/src/routes/sign-in.tsx +10 -3
- package/templates/web/src/routes/sign-up.tsx +4 -0
- package/templates/web/src/routes/verify-email.tsx +20 -0
- package/templates/web/test/api-contract/auth.spec.ts +155 -0
- package/templates/web/test/app.spec.tsx +47 -22
- package/templates/web/test/features/auth/email-flows.spec.tsx +121 -0
- package/templates/web/test/features/auth/sign-in-form.spec.tsx +6 -2
- package/templates/web/test/features/auth/social-buttons.spec.tsx +35 -0
- package/templates/web/test/features/oauth/consent-form.spec.tsx +90 -26
- package/templates/web/test/support/render-app.tsx +4 -4
- package/templates/workspace/scripts/ports.mjs +1 -1
- package/dist/mcp.js +0 -100
- package/templates/api/docker/init/01-auth-schema.sql +0 -1
- package/templates/api/migrations/Migration20260914123239_create_profile.ts +0 -13
- package/templates/api/src/infra/auth/auth.cli.ts +0 -5
- package/templates/api/src/infra/auth/auth.factory.ts +0 -77
- package/templates/api/src/infra/database/mikroorm/entities/auth-user.schema.ts +0 -16
- package/templates/api/test/infra/auth/auth.factory.spec.ts +0 -22
- package/templates/mobile/src/features/auth/auth-client.ts +0 -27
- package/templates/mobile/src/features/auth/session.ts +0 -17
- package/templates/web/src/features/auth/auth-client.ts +0 -17
- package/templates/web/src/features/auth/session.ts +0 -17
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Rule
|
|
4
4
|
|
|
5
|
-
Put in `expo-secure-store` anything that **grants access if stolen**: the
|
|
5
|
+
Put in `expo-secure-store` anything that **grants access if stolen**: the access and refresh tokens, and any
|
|
6
|
+
other token.
|
|
6
7
|
Put everything else in MMKV.
|
|
7
8
|
|
|
8
9
|
Do not use MMKV's own encryption.
|
|
9
10
|
|
|
10
11
|
Persist the TanStack Query cache to MMKV with a maximum age.
|
|
11
12
|
|
|
12
|
-
On logout, clear the
|
|
13
|
+
On logout, clear the tokens, the persisted cache, and every MMKV key holding that user's data. Leave
|
|
13
14
|
device preferences (theme, language) untouched.
|
|
14
15
|
|
|
15
16
|
Decide when writing each key whether it belongs to the user or to the device.
|
|
@@ -33,7 +34,7 @@ from one that opens blank with a spinner, which is what happens every time someb
|
|
|
33
34
|
maximum age exists because a cache without one presents months-old data as current with no way for the
|
|
34
35
|
user to tell.
|
|
35
36
|
|
|
36
|
-
That persistence is also why logout must clear more than the
|
|
37
|
+
That persistence is also why logout must clear more than the tokens: switching users on one device would
|
|
37
38
|
otherwise show the previous user's data. Wiping MMKV wholesale instead would take theme and language with
|
|
38
39
|
it, so the same person returning finds the app as if it were new; security that punishes the common case
|
|
39
40
|
gets switched off by somebody.
|
|
@@ -47,7 +48,7 @@ Every write to persistent storage, and the logout path.
|
|
|
47
48
|
Choosing a store:
|
|
48
49
|
|
|
49
50
|
```
|
|
50
|
-
✅ SecureStore: the
|
|
51
|
+
✅ SecureStore: the access and refresh tokens
|
|
51
52
|
MMKV: theme, last opened tab, cached responses
|
|
52
53
|
❌ MMKV: the session token // works perfectly, protects nothing
|
|
53
54
|
```
|
|
@@ -62,8 +63,8 @@ Opening MMKV (version 4):
|
|
|
62
63
|
Logout:
|
|
63
64
|
|
|
64
65
|
```
|
|
65
|
-
✅ clear
|
|
66
|
-
❌ clear
|
|
66
|
+
✅ clear tokens + query cache + user-scoped MMKV keys, kept as one list the logout walks
|
|
67
|
+
❌ clear tokens only
|
|
67
68
|
❌ MMKV.clearAll() // takes theme and language too
|
|
68
69
|
```
|
|
69
70
|
|
|
@@ -30,9 +30,9 @@ Dockerfile.
|
|
|
30
30
|
The integration package exists because the rest of the sharing was already refused: contract types are
|
|
31
31
|
hand-written rather than generated, validation differs on each side, and shadcn copies components rather
|
|
32
32
|
than sharing them. What genuinely repeats across clients is how a URL is built, how an error body becomes
|
|
33
|
-
an `ApiError`,
|
|
34
|
-
automatically, mobile
|
|
35
|
-
comes from outside.
|
|
33
|
+
an `ApiError`, what the contract's shapes are, and how an expired access token is refreshed. Only transport
|
|
34
|
+
differs (the web sends its cookies automatically, mobile hands the auth client a store over secure storage, a
|
|
35
|
+
site's server calls anonymously), so transport comes from outside.
|
|
36
36
|
|
|
37
37
|
**This is also what recovers most of what hand-written types gave up.** That decision accepted, as its
|
|
38
38
|
cost, that a renamed field leaves each client compiling against its own copy. One copy instead of three
|
|
@@ -65,7 +65,7 @@ The package's boundary:
|
|
|
65
65
|
|
|
66
66
|
```
|
|
67
67
|
✅ createClient({ fetch: myTransport })
|
|
68
|
-
❌ the package calling fetch and reading
|
|
68
|
+
❌ the package calling fetch and reading secure storage itself
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
## Enforcement
|
|
@@ -17,7 +17,7 @@ Never read the tenant from local storage or from a cached response to decide wha
|
|
|
17
17
|
|
|
18
18
|
The query-key rule already says a key includes everything that changes the result, and the tenant changes
|
|
19
19
|
the result. **But the tenant is invisible to whoever writes the query:** the server derives it from the
|
|
20
|
-
|
|
20
|
+
access token, so the client never sends it, never passes it as a filter, and never sees it in the calling code.
|
|
21
21
|
Nobody remembers to put it in a key, because nothing in the file suggests it exists. Adding it everywhere
|
|
22
22
|
would be correct when remembered and silent when not; clearing once, in one place, does not depend on
|
|
23
23
|
memory.
|
|
@@ -45,10 +45,10 @@ Every client of a multi-tenant project: `web`, `mobile` and `site` alike.
|
|
|
45
45
|
Switching:
|
|
46
46
|
|
|
47
47
|
```
|
|
48
|
-
✅ await
|
|
48
|
+
✅ await switchTenant(organizationId) // the API re-issues the access token
|
|
49
49
|
queryClient.clear()
|
|
50
50
|
router.navigate({ to: '/' })
|
|
51
|
-
❌ await
|
|
51
|
+
❌ await switchTenant(organizationId) // cache and route untouched
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Query keys:
|
|
@@ -78,6 +78,6 @@ client may choose.
|
|
|
78
78
|
That the active tenant is on screen. Its absence breaks nothing and costs somebody a wrong action in the
|
|
79
79
|
wrong account.
|
|
80
80
|
|
|
81
|
-
**Known limit:** if the session's tenant changes without this app knowing (another tab switching),
|
|
82
|
-
|
|
83
|
-
decision accepted.
|
|
81
|
+
**Known limit:** if the session's tenant changes without this app knowing (another tab switching), this tab
|
|
82
|
+
keeps its access token, and its tenant, until the next refresh, and then its cache is not cleared. That follows
|
|
83
|
+
from the active organization living in the session, and is the cost that decision accepted.
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
Derive the active tenant from the session. **Never from anything the client sends**: not a header, not a
|
|
6
6
|
path parameter, not the `Host`.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
Keep organizations in the `auth` module: an `Organization` entity, and a `Membership` joining a user to one.
|
|
9
|
+
Store the active tenant on the `Session` row, as `active_organization_id`, and copy it into the access token as
|
|
10
|
+
a claim each time one is issued or refreshed. Switch tenants through one route that checks the membership,
|
|
11
|
+
updates the session and issues a new access token. Do not manage the active organization client-side.
|
|
10
12
|
|
|
11
13
|
Give every tenant-scoped table a `tenant_id` that is `NOT NULL` and carries a foreign key into
|
|
12
14
|
`organization`, with `ON DELETE RESTRICT`.
|
|
13
15
|
|
|
14
16
|
Register the MikroORM filter with `default: true` and set its parameter once per request, from the
|
|
15
|
-
|
|
17
|
+
verified access token's claim.
|
|
16
18
|
|
|
17
19
|
**Never accept `tenant_id` as a method argument.** Read it where the filter's parameter is read.
|
|
18
20
|
|
|
@@ -22,7 +24,8 @@ a comment saying it was checked.
|
|
|
22
24
|
Declare a route that has no tenant. **Treat an absent filter parameter as an error**, never as "no
|
|
23
25
|
filter".
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
When a membership ends, revoke that user's sessions with it, or the departed member keeps the tenant until the
|
|
28
|
+
access token expires.
|
|
26
29
|
|
|
27
30
|
## Rationale
|
|
28
31
|
|
|
@@ -32,9 +35,13 @@ forgetting is invisible, because the route works perfectly for someone entitled
|
|
|
32
35
|
someone who is not. A subdomain is input too: it arrives in a header a client controls and that proxies
|
|
33
36
|
rewrite, so the defence would rest on infrastructure configuration.
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
a multi-tenant system that means the **tab** decides isolation, so it is refused here by name:
|
|
37
|
-
will find that
|
|
38
|
+
Keeping the active organization in the client, per tab, is a common suggestion for working in two tenants at
|
|
39
|
+
once. For a multi-tenant system that means the **tab** decides isolation, so it is refused here by name:
|
|
40
|
+
somebody will find that suggestion.
|
|
41
|
+
|
|
42
|
+
The claim is read from a token whose signature was just verified, so no request touches the database to learn
|
|
43
|
+
its tenant. The cost is the token's lifetime: a membership removed now still reads for up to fifteen minutes,
|
|
44
|
+
unless the sessions go with it, which is why the rule asks for both.
|
|
38
45
|
|
|
39
46
|
The filter is an application-level guard, and its own documentation says a raw query or a forgotten
|
|
40
47
|
`filters` option reads past it. It leaves three holes and they get different answers, because the problems
|
|
@@ -48,8 +55,8 @@ did not load, middleware in the wrong order) into a query that returns the whole
|
|
|
48
55
|
|
|
49
56
|
The foreign key exists because without it a `tenant_id` pointing nowhere inserts cleanly and passes tests,
|
|
50
57
|
leaving a row invisible to every filtered query. `RESTRICT` means deleting an organization with business
|
|
51
|
-
data fails, which is wanted: the application should decide what happens to that data rather than a
|
|
52
|
-
|
|
58
|
+
data fails, which is wanted: the application should decide what happens to that data rather than a cascade
|
|
59
|
+
deciding for it.
|
|
53
60
|
|
|
54
61
|
## Applies to
|
|
55
62
|
|
|
@@ -64,7 +71,7 @@ decision rather than configuration.
|
|
|
64
71
|
Where the tenant comes from:
|
|
65
72
|
|
|
66
73
|
```
|
|
67
|
-
✅ const tenantId =
|
|
74
|
+
✅ const tenantId = claims.organizationId // from the verified access token
|
|
68
75
|
❌ const tenantId = req.headers['x-tenant-id']
|
|
69
76
|
❌ const tenantId = subdomainOf(req.headers.host)
|
|
70
77
|
```
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Social sign-in
|
|
2
|
+
|
|
3
|
+
## Rule
|
|
4
|
+
|
|
5
|
+
Sign in with a provider through the API, never from a client: the client opens
|
|
6
|
+
`/api/auth/social/<provider>?client=web|mobile`, the API runs OpenID Connect with `openid-client` behind the
|
|
7
|
+
`IdentityProviders` port, and the client comes back signed in. Add a provider in the adapter's `settings()`, its
|
|
8
|
+
variables in `env.ts`, and its button on each client.
|
|
9
|
+
|
|
10
|
+
Store each trip in `social_sign_in`: its id is OAuth's `state`, and the browser that started it holds a secret
|
|
11
|
+
in the `social_sign_in` cookie that the callback must present. Use PKCE wherever the provider's metadata announces
|
|
12
|
+
it, and a nonce always. Turn Apple's `form_post` callback into a redirect to the same address, so the cookie
|
|
13
|
+
comes with it.
|
|
14
|
+
|
|
15
|
+
Return the web to the path it asked for, only a path on the web app, with session cookies. Return a mobile app
|
|
16
|
+
to `MOBILE_APP_SCHEME://social` with a one-time code it exchanges at `/api/auth/social/exchange` for its tokens,
|
|
17
|
+
within two minutes. A mobile app opens the trip with `expo-web-browser`'s `openAuthSessionAsync`.
|
|
18
|
+
|
|
19
|
+
Recognise a returning user by the provider's subject, kept in `identity`. For a first sign-in, link by email
|
|
20
|
+
only when the provider says `email_verified`:
|
|
21
|
+
|
|
22
|
+
- an account whose email was confirmed is the same person: link it;
|
|
23
|
+
- an account whose email was never confirmed loses its password and every session, then is linked: the
|
|
24
|
+
provider's proof wins over whoever chose that password;
|
|
25
|
+
- no account: create one, confirmed and without a password, with its profile.
|
|
26
|
+
|
|
27
|
+
Refuse a provider email that is not verified.
|
|
28
|
+
|
|
29
|
+
When the project has a mobile app and offers Google, offer Apple too before publishing on iOS.
|
|
30
|
+
|
|
31
|
+
## Rationale
|
|
32
|
+
|
|
33
|
+
The API keeps the client secret and validates the ID token, so neither a browser nor an app ever holds a
|
|
34
|
+
provider's secret or decides what an ID token says.
|
|
35
|
+
|
|
36
|
+
The cookie binds the trip to one browser. Without it, someone could start a sign-in with their own provider
|
|
37
|
+
account and get a victim to open the callback, signing the victim into the attacker's account, which then
|
|
38
|
+
records whatever the victim does. The mobile code exists so that tokens never travel in a URL, where logs and
|
|
39
|
+
history keep them.
|
|
40
|
+
|
|
41
|
+
The linking rules close a known attack: register a password account with somebody's email before they arrive,
|
|
42
|
+
then wait for them to sign in with a provider. A library this project once used shipped exactly that hole. An
|
|
43
|
+
account nobody confirmed belongs to whoever proves the inbox, and that proof is what the provider gives.
|
|
44
|
+
|
|
45
|
+
The App Store asks an app offering a third-party login for its primary account to offer an equivalent
|
|
46
|
+
privacy-preserving login too (App Review Guideline 4.8); Sign in with Apple is one.
|
|
47
|
+
|
|
48
|
+
**What this costs:** the trip leaves the app for the system browser and comes back, rather than a native sheet.
|
|
49
|
+
In a project that does not verify emails, no password account is ever confirmed, so the first time its owner
|
|
50
|
+
signs in with a provider for that email, the password goes: the owner keeps the account through the provider,
|
|
51
|
+
and cannot tell them apart from whoever registered the email first.
|
|
52
|
+
And a provider without its variables in `.env` answers that it is not configured, until someone registers the
|
|
53
|
+
application with it.
|
|
54
|
+
|
|
55
|
+
## Applies to
|
|
56
|
+
|
|
57
|
+
`src/domain/auth/` for identities and trips, `social.controller.ts`, `src/infra/social/`, and the social buttons on
|
|
58
|
+
the web and mobile.
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
✅ a href={auth.socialSignInUrl('google', 'web', redirect)}
|
|
64
|
+
❌ window.location = 'https://accounts.google.com/o/oauth2/v2/auth?client_id=…' // the client talks to Google
|
|
65
|
+
|
|
66
|
+
✅ link only when email_verified, and void an unconfirmed account's password
|
|
67
|
+
❌ link whenever the emails match
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Enforcement
|
|
71
|
+
|
|
72
|
+
**Tests.** `social.use-case.spec.ts` covers each linking rule and refuses a callback from another browser, a
|
|
73
|
+
second use and another provider. `openid-identity-providers.adapter.spec.ts` runs the real adapter against a
|
|
74
|
+
local OpenID provider: PKCE, state, nonce, a forged code, and Apple's client secret signed with the `.p8` key.
|
|
75
|
+
`social.controller.spec.ts` covers both clients over HTTP.
|
|
76
|
+
|
|
77
|
+
**Review only.** That a new provider's ID token is validated by `openid-client` before anything reads it.
|
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ terminal a missing answer is an error rather than a default, so nothing importan
|
|
|
58
58
|
silence:
|
|
59
59
|
|
|
60
60
|
```sh
|
|
61
|
-
npx @stjoseph/prumo new my-app --types api,web --single-tenant --no-mcp
|
|
61
|
+
npx @stjoseph/prumo new my-app --types api,web --single-tenant --no-mcp --email --no-social
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
| Flag | What it answers |
|
|
@@ -67,6 +67,8 @@ npx @stjoseph/prumo new my-app --types api,web --single-tenant --no-mcp
|
|
|
67
67
|
| `--alone` or `--monorepo` | How a single type is laid out |
|
|
68
68
|
| `--multi-tenant` or `--single-tenant` | Whether the application serves several tenants. Multi-tenant ships the `multi-tenancy` conventions; the generated code itself is not tenant-aware yet |
|
|
69
69
|
| `--mcp` or `--no-mcp` | With an api and a web: whether AI assistants reach the API through MCP, signed in as the user |
|
|
70
|
+
| `--email` or `--no-email` | With an api: email verification and password reset, by a 6-digit code sent through the `Mailer` port, which writes to the log until you give it a provider |
|
|
71
|
+
| `--social google,apple` or `--no-social` | With an api and a web or mobile: sign-in with Google, Apple, or both |
|
|
70
72
|
| `--skip-install` | Stops after writing the files |
|
|
71
73
|
|
|
72
74
|
## Commands
|
|
@@ -88,9 +90,9 @@ is asked, and the exit code is 0 only when `ok` is true.
|
|
|
88
90
|
|
|
89
91
|
| Type | What you get |
|
|
90
92
|
|---|---|
|
|
91
|
-
| `api` | Fastify with a domain/infra structure, tsyringe, Zod, MikroORM on PostgreSQL,
|
|
93
|
+
| `api` | Fastify with a domain/infra structure, tsyringe, Zod, MikroORM on PostgreSQL, its own JWT authentication, Swagger UI, Vitest with Testcontainers |
|
|
92
94
|
| `web` | Vite, React, TanStack Router and Query, Tailwind, shadcn/ui, forms validated with Zod |
|
|
93
|
-
| `mobile` | Expo with Expo Router, NativeWind, MMKV, and the
|
|
95
|
+
| `mobile` | Expo with Expo Router, NativeWind, MMKV, and the tokens kept in `expo-secure-store` |
|
|
94
96
|
| `site` | Next on the App Router, static by default, for pages that have to be indexed |
|
|
95
97
|
|
|
96
98
|
One type produces that project on its own. Several produce a pnpm workspace: each application under
|
|
@@ -106,13 +108,13 @@ a month apart are the same project.
|
|
|
106
108
|
|
|
107
109
|
| File | What it is |
|
|
108
110
|
|---|---|
|
|
109
|
-
| `.prumo/` | The conventions, in
|
|
111
|
+
| `.prumo/` | The conventions, in twelve areas. Only the areas the answers call for are copied |
|
|
110
112
|
| `.prumo/INDEX.md` | One line per document. Generated by the tool, and safe to regenerate |
|
|
111
113
|
| `AGENTS.md` | The pointer into `.prumo/`, read by Codex, Cursor, Copilot, Gemini CLI and others |
|
|
112
114
|
| `CLAUDE.md` | An `@AGENTS.md` import, for Claude Code |
|
|
113
115
|
|
|
114
|
-
The
|
|
115
|
-
`multi-tenancy` and `
|
|
116
|
+
The twelve areas are `core`, `api`, `database`, `client`, `web`, `mobile`, `site`, `monorepo`,
|
|
117
|
+
`multi-tenancy`, `mcp`, `email` and `social`. Only `core` ships unconditionally, because everything in it is true of every type;
|
|
116
118
|
the rest follow the answers, since advice about a platform the project does not have is worse than
|
|
117
119
|
no advice at all.
|
|
118
120
|
|
package/dist/asker.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as prompt from '@clack/prompts';
|
|
2
|
+
import { SOCIAL_PROVIDERS } from "./features.js";
|
|
2
3
|
const TYPES = ['api', 'web', 'mobile', 'site'];
|
|
3
4
|
function answered(value) {
|
|
4
5
|
if (prompt.isCancel(value)) {
|
|
@@ -28,5 +29,17 @@ export function terminalAsker(validateName) {
|
|
|
28
29
|
message: 'Let AI assistants use the API through MCP, signed in as the user?',
|
|
29
30
|
initialValue: false,
|
|
30
31
|
})),
|
|
32
|
+
email: async () => answered(await prompt.confirm({
|
|
33
|
+
message: 'Verify emails and let users reset their password?',
|
|
34
|
+
initialValue: false,
|
|
35
|
+
})),
|
|
36
|
+
social: async () => answered(await prompt.multiselect({
|
|
37
|
+
message: 'Social sign-in? Leave all unselected for none',
|
|
38
|
+
options: SOCIAL_PROVIDERS.map((provider) => ({
|
|
39
|
+
value: provider,
|
|
40
|
+
label: provider === 'google' ? 'Google' : 'Apple',
|
|
41
|
+
})),
|
|
42
|
+
required: false,
|
|
43
|
+
})),
|
|
31
44
|
};
|
|
32
45
|
}
|
package/dist/clean.js
CHANGED
|
@@ -10,7 +10,7 @@ export const API_DEV_PREFIX = 'node scripts/database.mjs --check && ';
|
|
|
10
10
|
export const ROOT_DEV_PREFIX = 'node apps/api/scripts/database.mjs --check && ';
|
|
11
11
|
export const SETUP_SCRIPT = 'node scripts/database.mjs';
|
|
12
12
|
export const EXAMPLE_BLOCK = '# MISSING until `pnpm db:setup` creates a database; `pnpm dev` offers to run it.\n' +
|
|
13
|
-
'DATABASE_URL=MISSING\
|
|
13
|
+
'DATABASE_URL=MISSING\n';
|
|
14
14
|
export const README_PARAGRAPH = '`DATABASE_URL` starts as `MISSING`. While it is, `pnpm dev` offers to create the database first: it asks for a name,\n' +
|
|
15
15
|
'starts Postgres from `docker-compose.yml` on port 5432 or the next free one (kept in `POSTGRES_PORT`), writes the URL\n' +
|
|
16
16
|
'into `.env`, and migrates. `pnpm db:setup` does the same at any time. Without a terminal every answer comes from a\n' +
|
|
@@ -51,7 +51,7 @@ function rulesFor(root, api, templates) {
|
|
|
51
51
|
const rules = [
|
|
52
52
|
devRule('api-dev-check', at('package.json'), API_DEV_PREFIX),
|
|
53
53
|
scriptRule('api-db-setup', at('package.json'), 'Remove the `db:setup` script', 'db:setup', (value) => value === SETUP_SCRIPT ? { status: 'pending' } : { status: 'modified', next: value }),
|
|
54
|
-
textRule('api-env-example', at('.env.example'), 'Put a sample database URL back in `.env.example`', EXAMPLE_BLOCK, `DATABASE_URL=${SAMPLE_URL}\
|
|
54
|
+
textRule('api-env-example', at('.env.example'), 'Put a sample database URL back in `.env.example`', EXAMPLE_BLOCK, `DATABASE_URL=${SAMPLE_URL}\n`, '=MISSING'),
|
|
55
55
|
textRule('api-readme-setup', at('README.md'), 'Replace the README paragraph about `MISSING`', README_PARAGRAPH, README_REPLACEMENT, 'db:setup'),
|
|
56
56
|
textRule('api-readme-row', at('README.md'), 'Remove `db:setup` from the README', README_ROW, '', '`pnpm db:setup`'),
|
|
57
57
|
];
|
package/dist/cli.js
CHANGED
|
@@ -34,6 +34,10 @@ async function runNew(args, json) {
|
|
|
34
34
|
'single-tenant': { type: 'boolean', default: false },
|
|
35
35
|
mcp: { type: 'boolean', default: false },
|
|
36
36
|
'no-mcp': { type: 'boolean', default: false },
|
|
37
|
+
email: { type: 'boolean', default: false },
|
|
38
|
+
'no-email': { type: 'boolean', default: false },
|
|
39
|
+
social: { type: 'string' },
|
|
40
|
+
'no-social': { type: 'boolean', default: false },
|
|
37
41
|
'skip-install': { type: 'boolean', default: false },
|
|
38
42
|
json: { type: 'boolean', default: false },
|
|
39
43
|
},
|
|
@@ -62,11 +66,21 @@ async function runNew(args, json) {
|
|
|
62
66
|
singleTenant: values['single-tenant'],
|
|
63
67
|
mcp: values.mcp,
|
|
64
68
|
noMcp: values['no-mcp'],
|
|
69
|
+
email: values.email,
|
|
70
|
+
noEmail: values['no-email'],
|
|
71
|
+
social: values.social,
|
|
72
|
+
noSocial: values['no-social'],
|
|
65
73
|
}, interactive ? terminalAsker((input) => projectProblem(input, cwd)) : undefined, cwd);
|
|
66
74
|
if (!json && answers.types.length > 1) {
|
|
67
75
|
prompt.log.info(`${answers.types.length} types make a workspace: apps/ and packages/ under one root.`);
|
|
68
76
|
}
|
|
69
|
-
const { target, ...chosen } = answers;
|
|
77
|
+
const { target, warnings, ...chosen } = answers;
|
|
78
|
+
// Without --json a person is reading; with it, the warnings travel in the result instead.
|
|
79
|
+
if (!json) {
|
|
80
|
+
for (const warning of warnings) {
|
|
81
|
+
prompt.log.warn(warning);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
70
84
|
await generate({
|
|
71
85
|
...requireAssets(assetsFor(import.meta.dirname)),
|
|
72
86
|
target,
|
|
@@ -77,7 +91,7 @@ async function runNew(args, json) {
|
|
|
77
91
|
if (!json) {
|
|
78
92
|
prompt.outro(`Created ${answers.name}.`);
|
|
79
93
|
}
|
|
80
|
-
return { data: { ...chosen, target, installed: install }, text: '' };
|
|
94
|
+
return { data: { ...chosen, target, installed: install, warnings }, text: '' };
|
|
81
95
|
}
|
|
82
96
|
async function runClean(args, json) {
|
|
83
97
|
const { values } = parseArgs({
|
package/dist/commands.js
CHANGED
|
@@ -18,6 +18,13 @@ export const COMMANDS = [
|
|
|
18
18
|
{ flag: '--single-tenant', description: 'No multi-tenancy conventions' },
|
|
19
19
|
{ flag: '--mcp', description: 'An MCP server on the api, authorized through the web' },
|
|
20
20
|
{ flag: '--no-mcp', description: 'No MCP server' },
|
|
21
|
+
{
|
|
22
|
+
flag: '--email',
|
|
23
|
+
description: 'Email verification and password reset, by a code sent through the Mailer port',
|
|
24
|
+
},
|
|
25
|
+
{ flag: '--no-email', description: 'Neither' },
|
|
26
|
+
{ flag: '--social <list>', description: 'Comma-separated: google, apple' },
|
|
27
|
+
{ flag: '--no-social', description: 'No social sign-in' },
|
|
21
28
|
{ flag: '--skip-install', description: 'Do not run pnpm install' },
|
|
22
29
|
JSON_OPTION,
|
|
23
30
|
],
|
package/dist/compose.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { cp, mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join, relative, sep } from 'node:path';
|
|
4
|
+
import { applyFeatures, NONE } from "./features.js";
|
|
4
5
|
import { prependProperty, readJsonc, removeWhatBaseDeclares, setJsonc } from "./jsonc.js";
|
|
5
|
-
import { applyMcp } from "./mcp.js";
|
|
6
6
|
const CLIENTS = ['web', 'mobile'];
|
|
7
7
|
// One exception to one-version-per-dependency: stable NativeWind needs Tailwind 3.
|
|
8
8
|
const NAMED_CATALOGS = [{ name: 'tailwind3', dependency: 'tailwindcss', major: '3' }];
|
|
@@ -182,14 +182,14 @@ async function assertCatalogCovers(target, catalog) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
-
export async function composeWorkspace({ templates, target, types,
|
|
185
|
+
export async function composeWorkspace({ templates, target, types, features = NONE, }) {
|
|
186
186
|
await mkdir(target, { recursive: true });
|
|
187
187
|
await copyTemplate(join(templates, 'workspace'), target);
|
|
188
188
|
const apps = [];
|
|
189
189
|
for (const type of types) {
|
|
190
190
|
const app = await copyApp(templates, target, type);
|
|
191
191
|
// Before the catalog is collected, so a dependency that leaves never reaches it.
|
|
192
|
-
await
|
|
192
|
+
await applyFeatures(app, type, features);
|
|
193
193
|
apps.push(app);
|
|
194
194
|
}
|
|
195
195
|
const clients = CLIENTS.filter((client) => types.includes(client));
|
|
@@ -199,14 +199,6 @@ export async function composeWorkspace({ templates, target, types, mobileScheme
|
|
|
199
199
|
else {
|
|
200
200
|
await rm(join(target, 'packages'), { recursive: true, force: true });
|
|
201
201
|
}
|
|
202
|
-
if (types.includes('api') && types.includes('mobile')) {
|
|
203
|
-
await rewriteText(join(target, 'apps', 'api', '.env.example'), (text) => {
|
|
204
|
-
if (!text.includes('# MOBILE_APP_SCHEME=app\n')) {
|
|
205
|
-
throw new Error('apps/api/.env.example no longer carries the commented MOBILE_APP_SCHEME line');
|
|
206
|
-
}
|
|
207
|
-
return text.replace('# MOBILE_APP_SCHEME=app\n', `MOBILE_APP_SCHEME=${mobileScheme}\n`);
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
202
|
// One command for everything and one per app. Each app answers to `dev`, so the root never needs to know how.
|
|
211
203
|
// The database and the ports are the exception: both may ask a question, which the interleaved output of a
|
|
212
204
|
// parallel run would bury, so the root asks first and each app's own check then finds nothing to do.
|
package/dist/context.js
CHANGED
|
@@ -12,6 +12,8 @@ export function areasFor(answers) {
|
|
|
12
12
|
...(answers.architecture === 'monorepo' ? ['monorepo'] : []),
|
|
13
13
|
...(answers.multiTenant ? ['multi-tenancy'] : []),
|
|
14
14
|
...(answers.mcp ? ['mcp'] : []),
|
|
15
|
+
...(answers.email ? ['email'] : []),
|
|
16
|
+
...(answers.social.length > 0 ? ['social'] : []),
|
|
15
17
|
];
|
|
16
18
|
}
|
|
17
19
|
async function titleOf(path) {
|
|
@@ -36,6 +38,8 @@ export async function writeContext(knowledge, root, answers) {
|
|
|
36
38
|
architecture: answers.architecture,
|
|
37
39
|
multiTenant: answers.multiTenant,
|
|
38
40
|
mcp: answers.mcp,
|
|
41
|
+
email: answers.email,
|
|
42
|
+
social: answers.social,
|
|
39
43
|
};
|
|
40
44
|
await writeFile(join(prumo, 'config.json'), `${JSON.stringify(config, null, 2)}\n`);
|
|
41
45
|
await writeFile(join(root, 'AGENTS.md'), `# ${answers.name}\n\nThis project's conventions live in \`.prumo/\`. Read [\`.prumo/INDEX.md\`](.prumo/INDEX.md) before changing how something is done.\n`);
|