@tulipes/cli 0.1.0 → 0.2.0-rc.1

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 (209) hide show
  1. package/README.md +11 -8
  2. package/dist/agent.d.ts +12 -0
  3. package/dist/agent.js +29 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/app-core.d.ts +8 -6
  6. package/dist/app-core.js +23 -14
  7. package/dist/app-core.js.map +1 -1
  8. package/dist/bin.js +3 -3
  9. package/dist/bin.js.map +1 -1
  10. package/dist/config-check.d.ts +8 -0
  11. package/dist/config-check.js +43 -0
  12. package/dist/config-check.js.map +1 -0
  13. package/dist/config-import.d.ts +20 -0
  14. package/dist/config-import.js +133 -0
  15. package/dist/config-import.js.map +1 -0
  16. package/dist/dev.d.ts +4 -4
  17. package/dist/dev.js +7 -10
  18. package/dist/dev.js.map +1 -1
  19. package/dist/init.d.ts +6 -11
  20. package/dist/init.js +17 -6420
  21. package/dist/init.js.map +1 -1
  22. package/dist/inspection.d.ts +9 -8
  23. package/dist/inspection.js +10 -11
  24. package/dist/inspection.js.map +1 -1
  25. package/dist/main.js +82 -11
  26. package/dist/main.js.map +1 -1
  27. package/dist/migrate.d.ts +30 -0
  28. package/dist/migrate.js +772 -0
  29. package/dist/migrate.js.map +1 -0
  30. package/dist/new-module.js +87 -46
  31. package/dist/new-module.js.map +1 -1
  32. package/dist/routes.d.ts +3 -5
  33. package/dist/routes.js +4 -6
  34. package/dist/routes.js.map +1 -1
  35. package/dist/spec.d.ts +2 -5
  36. package/dist/spec.js +13 -6
  37. package/dist/spec.js.map +1 -1
  38. package/dist/sync.d.ts +3 -11
  39. package/dist/sync.js +81 -20
  40. package/dist/sync.js.map +1 -1
  41. package/dist/template.d.ts +58 -0
  42. package/dist/template.js +244 -0
  43. package/dist/template.js.map +1 -0
  44. package/dist/update.js +4 -4
  45. package/package.json +17 -16
  46. package/templates/AGENTS.md +162 -0
  47. package/templates/CLAUDE.md +11 -118
  48. package/templates/app/.envs/.env.development +18 -0
  49. package/templates/app/.envs/.env.production +19 -0
  50. package/templates/app/.envs/.env.staging +19 -0
  51. package/templates/app/README.md +327 -0
  52. package/templates/app/app.ts +3 -0
  53. package/templates/app/config/app.config.ts +73 -0
  54. package/templates/app/docs/agent-manifest.json +724 -0
  55. package/templates/app/ecosystem.config.cjs +60 -0
  56. package/templates/app/modules/agent/controllers/agent.controllers.ts +31 -0
  57. package/templates/app/modules/agent/i18n/en.json +5 -0
  58. package/templates/app/modules/agent/i18n/fr.json +5 -0
  59. package/templates/app/modules/agent/module.acl.ts +14 -0
  60. package/templates/app/modules/agent/package.json +21 -0
  61. package/templates/app/modules/agent/routes/agent.routes.ts +29 -0
  62. package/templates/app/modules/agent/schemas/agent.schemas.ts +9 -0
  63. package/templates/app/modules/auth/bootstrap/credentials.bootstrap.ts +26 -0
  64. package/templates/app/modules/auth/bootstrap/seed-admin.bootstrap.ts +44 -0
  65. package/templates/app/modules/auth/controllers/auth.controllers.ts +312 -0
  66. package/templates/app/modules/auth/helpers/access-check.ts +42 -0
  67. package/templates/app/modules/auth/helpers/auth.ts +48 -0
  68. package/templates/app/modules/auth/helpers/documents.ts +57 -0
  69. package/templates/app/modules/auth/helpers/passport.ts +180 -0
  70. package/templates/app/modules/auth/helpers/passwords.ts +58 -0
  71. package/templates/app/modules/auth/helpers/session-cache.ts +67 -0
  72. package/templates/app/modules/auth/helpers/tokens.ts +329 -0
  73. package/templates/app/modules/auth/i18n/en.json +19 -0
  74. package/templates/app/modules/auth/i18n/fr.json +19 -0
  75. package/templates/app/modules/auth/lib/index.ts +12 -0
  76. package/templates/app/modules/auth/models/credential.model.ts +31 -0
  77. package/templates/app/modules/auth/models/revoked-token.model.ts +20 -0
  78. package/templates/app/modules/auth/models/session.model.ts +42 -0
  79. package/templates/app/modules/auth/module.acl.ts +41 -0
  80. package/templates/app/modules/auth/module.config.ts +30 -0
  81. package/templates/app/modules/auth/package.json +61 -0
  82. package/templates/app/modules/auth/routes/auth.routes.ts +94 -0
  83. package/templates/app/modules/auth/schemas/auth.schemas.ts +37 -0
  84. package/templates/app/modules/auth/tests/auth.test.ts +1079 -0
  85. package/templates/app/modules/config/controllers/config.controllers.ts +89 -0
  86. package/templates/app/modules/config/helpers/audit.ts +41 -0
  87. package/templates/app/modules/config/helpers/describe.ts +72 -0
  88. package/templates/app/modules/config/i18n/en.json +6 -0
  89. package/templates/app/modules/config/i18n/fr.json +6 -0
  90. package/templates/app/modules/config/models/config-audit.model.ts +26 -0
  91. package/templates/app/modules/config/module.acl.ts +12 -0
  92. package/templates/app/modules/config/package.json +23 -0
  93. package/templates/app/modules/config/routes/config.routes.ts +37 -0
  94. package/templates/app/modules/config/schemas/config.schemas.ts +13 -0
  95. package/templates/app/modules/config/tests/config.test.ts +126 -0
  96. package/templates/app/modules/core/bootstrap/cache.bootstrap.ts +12 -0
  97. package/templates/app/modules/core/helpers/cache/index.ts +60 -0
  98. package/templates/app/modules/core/helpers/cache/memory-cache.ts +53 -0
  99. package/templates/app/modules/core/helpers/cache/redis-cache.ts +68 -0
  100. package/templates/app/modules/core/helpers/cache/types.ts +37 -0
  101. package/templates/app/modules/core/helpers/lazy.ts +13 -0
  102. package/templates/app/modules/core/helpers/logger/index.ts +115 -0
  103. package/templates/app/modules/core/helpers/mongoose/index.ts +27 -0
  104. package/templates/app/modules/core/helpers/mongoose/plugins/base.plugin.ts +14 -0
  105. package/templates/app/modules/core/helpers/mongoose/plugins/soft-delete.plugin.ts +124 -0
  106. package/templates/app/modules/core/helpers/mongoose/plugins/timestamps.plugin.ts +13 -0
  107. package/templates/app/modules/core/helpers/mongoose/plugins/types.ts +45 -0
  108. package/templates/app/modules/core/helpers/mongoose/register.ts +20 -0
  109. package/templates/app/modules/core/lib/index.ts +34 -0
  110. package/templates/app/modules/core/module.config.ts +24 -0
  111. package/templates/app/modules/core/package.json +80 -0
  112. package/templates/app/modules/core/routes/core.routes.ts +24 -0
  113. package/templates/app/modules/core/tests/core.test.ts +19 -0
  114. package/templates/app/modules/hello/agent/hello.agent.ts +33 -0
  115. package/templates/app/modules/hello/bootstrap/seed-greetings.bootstrap.ts +27 -0
  116. package/templates/app/modules/hello/controllers/greetings.controllers.ts +43 -0
  117. package/templates/app/modules/hello/controllers/hello.controllers.ts +50 -0
  118. package/templates/app/modules/hello/helpers/greeting.ts +8 -0
  119. package/templates/app/modules/hello/i18n/en.json +8 -0
  120. package/templates/app/modules/hello/i18n/fr.json +8 -0
  121. package/templates/app/modules/hello/models/greeting.model.ts +24 -0
  122. package/templates/app/modules/hello/module.acl.ts +16 -0
  123. package/templates/app/modules/hello/module.config.ts +14 -0
  124. package/templates/app/modules/hello/package.json +24 -0
  125. package/templates/app/modules/hello/queues/hello.queues.ts +38 -0
  126. package/templates/app/modules/hello/routes/greetings.routes.ts +25 -0
  127. package/templates/app/modules/hello/routes/hello.routes.ts +44 -0
  128. package/templates/app/modules/hello/sockets/hello.sockets.ts +21 -0
  129. package/templates/app/modules/hello/tests/hello.test.ts +19 -0
  130. package/templates/app/modules/security/bootstrap/rate-limit.bootstrap.ts +8 -0
  131. package/templates/app/modules/security/helpers/hardening.ts +65 -0
  132. package/templates/app/modules/security/helpers/proxy.ts +16 -0
  133. package/templates/app/modules/security/helpers/rate-limit.ts +150 -0
  134. package/templates/app/modules/security/module.config.ts +59 -0
  135. package/templates/app/modules/security/package.json +26 -0
  136. package/templates/app/modules/security/routes/security.routes.ts +42 -0
  137. package/templates/app/modules/security/tests/security.test.ts +174 -0
  138. package/templates/app/modules/settings/bootstrap/log-streams.bootstrap.ts +12 -0
  139. package/templates/app/modules/settings/controllers/settings.controllers.ts +58 -0
  140. package/templates/app/modules/settings/helpers/access-lease.ts +99 -0
  141. package/templates/app/modules/settings/helpers/log-stream.ts +272 -0
  142. package/templates/app/modules/settings/helpers/log-tail.ts +253 -0
  143. package/templates/app/modules/settings/i18n/en.json +3 -0
  144. package/templates/app/modules/settings/i18n/fr.json +3 -0
  145. package/templates/app/modules/settings/module.acl.ts +12 -0
  146. package/templates/app/modules/settings/package.json +21 -0
  147. package/templates/app/modules/settings/routes/settings.routes.ts +33 -0
  148. package/templates/app/modules/settings/schemas/settings.schemas.ts +8 -0
  149. package/templates/app/modules/settings/tests/settings.test.ts +409 -0
  150. package/templates/app/modules/users/controllers/users.controllers.ts +113 -0
  151. package/templates/app/modules/users/i18n/en.json +14 -0
  152. package/templates/app/modules/users/i18n/fr.json +14 -0
  153. package/templates/app/modules/users/models/user.model.ts +18 -0
  154. package/templates/app/modules/users/module.acl.ts +10 -0
  155. package/templates/app/modules/users/module.config.ts +11 -0
  156. package/templates/app/modules/users/package.json +24 -0
  157. package/templates/app/modules/users/queues/users.queues.ts +31 -0
  158. package/templates/app/modules/users/routes/users.routes.ts +60 -0
  159. package/templates/app/modules/users/schemas/users.schemas.ts +30 -0
  160. package/templates/app/modules/users/sockets/users.sockets.ts +18 -0
  161. package/templates/app/nginx/development.conf +44 -0
  162. package/templates/app/nginx/production.conf +75 -0
  163. package/templates/app/nginx/staging.conf +51 -0
  164. package/templates/app/package.json +58 -0
  165. package/templates/app/scripts/count-greetings.ts +29 -0
  166. package/templates/app/scripts/deploy-nginx.ts +143 -0
  167. package/templates/app/scripts/list-users.ts +34 -0
  168. package/templates/app/tests/app.test.ts +28 -0
  169. package/templates/app/tests/support.ts +36 -0
  170. package/templates/app/tsconfig.json +28 -0
  171. package/templates/app/vitest.config.ts +6 -0
  172. package/templates/app/worker.ts +6 -0
  173. package/templates/claude/skills/tulipes-agent-tool/SKILL.md +83 -0
  174. package/templates/claude/skills/tulipes-boot-errors/SKILL.md +11 -5
  175. package/templates/claude/skills/tulipes-config/SKILL.md +74 -0
  176. package/templates/claude/skills/tulipes-endpoint/SKILL.md +38 -25
  177. package/templates/claude/skills/tulipes-env-variable/SKILL.md +54 -26
  178. package/templates/claude/skills/tulipes-model/SKILL.md +11 -4
  179. package/templates/claude/skills/tulipes-module/SKILL.md +11 -10
  180. package/templates/claude/skills/tulipes-queue/SKILL.md +20 -6
  181. package/templates/claude/skills/tulipes-settings/SKILL.md +49 -100
  182. package/templates/claude/skills/tulipes-spec/SKILL.md +10 -12
  183. package/templates/minimal/README.md +21 -0
  184. package/templates/minimal/app.ts +3 -0
  185. package/templates/minimal/config/app.config.ts +16 -0
  186. package/templates/minimal/modules/agent/module.acl.ts +3 -0
  187. package/templates/minimal/modules/agent/package.json +14 -0
  188. package/templates/minimal/modules/agent/routes/agent.routes.ts +20 -0
  189. package/templates/minimal/modules/core/module.acl.ts +4 -0
  190. package/templates/minimal/modules/core/package.json +21 -0
  191. package/templates/minimal/modules/hello/i18n/en.json +3 -0
  192. package/templates/minimal/modules/hello/module.acl.ts +2 -0
  193. package/templates/minimal/modules/hello/package.json +11 -0
  194. package/templates/minimal/modules/hello/routes/health.routes.ts +9 -0
  195. package/templates/minimal/modules/hello/routes/hello.routes.ts +10 -0
  196. package/templates/minimal/modules/security/package.json +15 -0
  197. package/templates/minimal/modules/security/routes/security.routes.ts +8 -0
  198. package/templates/minimal/package.json +38 -0
  199. package/templates/minimal/tests/app.test.ts +52 -0
  200. package/templates/minimal/tsconfig.json +22 -0
  201. package/templates/minimal/vitest.config.ts +5 -0
  202. package/templates/overlays/no-queues/ecosystem.config.cjs +52 -0
  203. package/templates/overlays/no-queues/modules/hello/controllers/greetings.controllers.ts +41 -0
  204. package/templates/overlays/no-queues/modules/users/controllers/users.controllers.ts +107 -0
  205. package/templates/versions.json +11 -0
  206. package/dist/minimal.d.ts +0 -2
  207. package/dist/minimal.js +0 -2200
  208. package/dist/minimal.js.map +0 -1
  209. /package/templates/{browser-auth.md → app/docs/browser-auth.md} +0 -0
@@ -0,0 +1,19 @@
1
+ {
2
+ "refreshUnavailable": "The refresh operation could not be acknowledged; sign in again",
3
+ "credentialsRequired": "An email address and a password are required",
4
+ "invalidCredentials": "Invalid email or password",
5
+ "passwordTooShort": "Password must be at least 8 characters",
6
+ "registrationClosed": "Registration is {{mode}}",
7
+ "emailUnavailable": "That email cannot be registered",
8
+ "refreshRequired": "A refresh token is required",
9
+ "refreshInvalid": "Invalid refresh token",
10
+ "refreshUsed": "That refresh token has already been used or revoked",
11
+ "accountGone": "This account no longer exists",
12
+ "loggedOut": "Signed out",
13
+ "passwordChanged": "Password changed — every other session was signed out",
14
+ "noSuchSession": "No such session",
15
+ "sessionEnded": "Session ended",
16
+ "emailRequired": "An email address is required",
17
+ "emailInvalid": "Must be a valid email address",
18
+ "passwordRequired": "A password is required"
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "refreshUnavailable": "Le renouvellement n’a pas pu être confirmé ; reconnectez-vous",
3
+ "credentialsRequired": "Une adresse email et un mot de passe sont requis",
4
+ "invalidCredentials": "Email ou mot de passe invalide",
5
+ "passwordTooShort": "Le mot de passe doit contenir au moins 8 caractères",
6
+ "registrationClosed": "Les inscriptions sont {{mode}}",
7
+ "emailUnavailable": "Cette adresse email ne peut pas être enregistrée",
8
+ "refreshRequired": "Un jeton de rafraîchissement est requis",
9
+ "refreshInvalid": "Jeton de rafraîchissement invalide",
10
+ "refreshUsed": "Ce jeton de rafraîchissement a déjà été utilisé ou révoqué",
11
+ "accountGone": "Ce compte n'existe plus",
12
+ "loggedOut": "Déconnecté",
13
+ "passwordChanged": "Mot de passe modifié — toutes les autres sessions ont été fermées",
14
+ "noSuchSession": "Session introuvable",
15
+ "sessionEnded": "Session terminée",
16
+ "emailRequired": "Une adresse email est requise",
17
+ "emailInvalid": "Doit être une adresse email valide",
18
+ "passwordRequired": "Un mot de passe est requis"
19
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * What the auth module offers other modules.
3
+ *
4
+ * No guards: routes are gated by `ctx.rai({ id })`, which core enforces at
5
+ * boot. Auth's contribution is resolving who the caller is — `req.auth`.
6
+ */
7
+ export { auth, currentUser, GUEST, type Auth } from "../helpers/auth.ts";
8
+ export { hashPassword, setPassword, verifyPassword } from "../helpers/passwords.ts";
9
+ export { endAllSessions, endSession, listSessions } from "../helpers/tokens.ts";
10
+ /** Call after writing a user's role or deleting the user: cached identities carry the old authority. */
11
+ export { forgetSession, forgetUser } from "../helpers/session-cache.ts";
12
+ export { createAccessCheck } from "../helpers/access-check.ts";
@@ -0,0 +1,31 @@
1
+ import { Schema } from "mongoose";
2
+ import type { ModelDef } from "@tulipes/mongoose";
3
+ import { baseSchemaOptions } from "@app/core";
4
+
5
+ /**
6
+ * Secrets live here, never on the User document.
7
+ *
8
+ * The separation is the point: a route that serialises a user cannot leak
9
+ * a password hash by accident, because the hash is not in that document at
10
+ * all. `select: false` is a second layer — even a query against this
11
+ * collection has to ask for the hash explicitly.
12
+ *
13
+ * One row per user, password only. API keys and OAuth links would be their
14
+ * own models rather than more columns here.
15
+ */
16
+ const credentialSchema = new Schema(
17
+ {
18
+ user: {
19
+ type: Schema.Types.ObjectId,
20
+ ref: "User",
21
+ required: true,
22
+ unique: true,
23
+ index: true,
24
+ },
25
+ version: { type: String, required: true },
26
+ password_hash: { type: String, required: true, select: false },
27
+ },
28
+ baseSchemaOptions,
29
+ );
30
+
31
+ export default { name: "Credential", schema: credentialSchema } satisfies ModelDef;
@@ -0,0 +1,20 @@
1
+ import { Schema } from "mongoose";
2
+ import type { ModelDef } from "@tulipes/mongoose";
3
+
4
+ /**
5
+ * Access tokens are stateless, so logging out cannot un-issue one. This is
6
+ * the blacklist that makes logout immediate: the access token's jti is
7
+ * recorded until its natural expiry, and every authenticated request
8
+ * checks it.
9
+ *
10
+ * Rows carry a TTL index set to the token's own expiry — once the token
11
+ * would have expired anyway, the row deletes itself and the list stays as
12
+ * small as the access-token lifetime allows. That is the argument for
13
+ * keeping JWT_ACCESS_TTL short.
14
+ */
15
+ const revokedTokenSchema = new Schema({
16
+ jti: { type: String, required: true, unique: true, index: true },
17
+ expires_at: { type: Date, required: true, expires: 0 },
18
+ });
19
+
20
+ export default { name: "RevokedToken", schema: revokedTokenSchema } satisfies ModelDef;
@@ -0,0 +1,42 @@
1
+ import { Schema } from "mongoose";
2
+ import type { ModelDef } from "@tulipes/mongoose";
3
+ import { baseSchemaOptions } from "@app/core";
4
+
5
+ /**
6
+ * One row per signed-in device — the server-side half of a stateless JWT.
7
+ *
8
+ * Both tokens of a pair carry this row's `sid`, which is what lets an
9
+ * ACCESS token find its own session: the refresh token's jti changes on
10
+ * every rotation, so it could never serve as the identifier. Keeping the
11
+ * row across rotations is the point — `last_seen_at`, `ip` and `user_agent`
12
+ * survive, and "sign out this device" is deleting one document.
13
+ *
14
+ * `refresh_jti` holds only the CURRENT refresh token. One atomic update
15
+ * either rotates that token or expires the family when a superseded one
16
+ * is reused. All access tokens share this row and lose authorization too.
17
+ * Each new login receives a separate sid, so other devices remain active.
18
+ *
19
+ * `expires_at` carries a TTL index, so mongo prunes dead sessions itself.
20
+ * `created_at` is not declared here — baseSchemaOptions provides it.
21
+ */
22
+ const sessionSchema = new Schema(
23
+ {
24
+ sid: { type: String, required: true, unique: true, index: true },
25
+ user: {
26
+ type: Schema.Types.ObjectId,
27
+ ref: "User",
28
+ required: true,
29
+ index: true,
30
+ },
31
+ credential_version: { type: String, required: true },
32
+ refresh_jti: { type: String, required: true, index: true },
33
+ /** Recorded at sign-in; a session that moves address keeps its origin. */
34
+ ip: { type: String },
35
+ user_agent: { type: String },
36
+ last_seen_at: { type: Date, default: Date.now },
37
+ expires_at: { type: Date, required: true, expires: 0 },
38
+ },
39
+ baseSchemaOptions,
40
+ );
41
+
42
+ export default { name: "Session", schema: sessionSchema } satisfies ModelDef;
@@ -0,0 +1,41 @@
1
+ import type { AclBuilder } from "@tulipes/core/acl";
2
+
3
+ /**
4
+ * The auth module owns the role vocabulary, because roles only mean
5
+ * anything alongside the thing that assigns them.
6
+ *
7
+ * `guest` is the fallback for a request with no valid token. That is what
8
+ * makes "public endpoint" an ordinary ACL grant rather than a second
9
+ * mechanism: login is public because guest may do it, not because a path
10
+ * appears on an allowlist somewhere else.
11
+ */
12
+ export default function authAcl(acl: AclBuilder): void {
13
+ acl.defineRole("guest").defineRole("user").defineRole("admin").defineRole("sysadmin");
14
+
15
+ // Anyone may attempt to authenticate. Refresh is public because the
16
+ // refresh token itself is the credential — an expired access token must
17
+ // not stand in the way of renewing it.
18
+ acl.allow("guest", "auth:login", "auth:register", "auth:refresh");
19
+
20
+ // Signed in: end your session, read yourself, change your own password,
21
+ // and manage the devices you are signed in on.
22
+ acl.allow(
23
+ "user",
24
+ "auth:logout",
25
+ "auth:me",
26
+ "auth:password",
27
+ "auth:sessions",
28
+ "auth:session-end",
29
+ );
30
+ // `admin` is granted each feature namespace EXPLICITLY rather than "*".
31
+ // A wildcard would also reach settings:*, and the infrastructure console
32
+ // is meant to be a boundary an ordinary administrator cannot cross — a
33
+ // grant that quietly includes everything makes "sysadmin only" a label
34
+ // rather than a rule. Add your own namespaces here as modules arrive.
35
+ acl.allow("admin", "auth:*", "users:*", "hello:*");
36
+
37
+ // `sysadmin` is an admin who can also administer the deployment. The
38
+ // settings grant is NOT here: a module grants its own namespace, so
39
+ // `settings:*` is declared by the settings module itself.
40
+ acl.allow("sysadmin", "auth:*", "users:*", "hello:*");
41
+ }
@@ -0,0 +1,30 @@
1
+ import { defineConfig } from "@tulipes/core/config";
2
+ import { z } from "zod/v4";
3
+
4
+ /**
5
+ * Token policy is a setting: every deployment wants to tune it, none of it
6
+ * is secret. The signing keys and the seeded admin credentials stay in the
7
+ * environment, where secrets belong, and `bootstrap/credentials.bootstrap.ts`
8
+ * refuses development values outside development.
9
+ */
10
+ export default defineConfig({
11
+ schema: z.object({
12
+ accessTtl: z.string().min(1).default("15m")
13
+ .describe("Access token lifetime, as an ms/jsonwebtoken duration. Keep it short: a revoked user keeps their role until it expires"),
14
+ refreshTtl: z.string().min(1).default("30d")
15
+ .describe("Refresh token lifetime; also the TTL on its database record"),
16
+ /**
17
+ * How stale a session's `last_seen_at` may get before a request pays for
18
+ * the write. One write per request is not an option; one per minute is.
19
+ */
20
+ sessionTouchSeconds: z.number().int().positive().default(60)
21
+ .describe("Seconds between two writes of a session's last_seen_at"),
22
+ /**
23
+ * A resolved token is kept in the app cache this long, bounded by
24
+ * accessTtl; 0 disables the cache. Revocations delete the entry, so
25
+ * the bound only matters while Redis is unreachable at that moment.
26
+ */
27
+ cacheTtlSeconds: z.number().int().min(0).max(3600).default(60)
28
+ .describe("Seconds a resolved session stays in the cache (0 disables it); never longer than accessTtl"),
29
+ }),
30
+ });
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@app/auth",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./lib/index.ts"
8
+ },
9
+ "tulipes": {
10
+ "tier": "sys",
11
+ "priority": 20,
12
+ "env": [
13
+ {
14
+ "name": "JWT_ACCESS_SECRET",
15
+ "type": "secret",
16
+ "group": "auth",
17
+ "required": true,
18
+ "description": "Signing key for short-lived access tokens"
19
+ },
20
+ {
21
+ "name": "JWT_REFRESH_SECRET",
22
+ "type": "secret",
23
+ "group": "auth",
24
+ "required": true,
25
+ "description": "Signing key for refresh tokens; separate from the access key so leaking one cannot mint the other"
26
+ },
27
+ {
28
+ "name": "AUTH_ADMIN_EMAIL",
29
+ "type": "string",
30
+ "group": "auth",
31
+ "required": true,
32
+ "description": "Email of the admin account seeded on first boot"
33
+ },
34
+ {
35
+ "name": "AUTH_ADMIN_PASSWORD",
36
+ "type": "secret",
37
+ "group": "auth",
38
+ "required": true,
39
+ "description": "Password for the seeded admin account; change it after the first login"
40
+ }
41
+ ]
42
+ },
43
+ "dependencies": {
44
+ "@app/core": "workspace:*",
45
+ "@tulipes/core": "workspace:*",
46
+ "@tulipes/mongoose": "workspace:*",
47
+ "bcryptjs": "^3",
48
+ "express": "^5",
49
+ "jsonwebtoken": "^9",
50
+ "mongoose": "^8",
51
+ "passport": "^0.7",
52
+ "passport-jwt": "^4",
53
+ "passport-local": "^1"
54
+ },
55
+ "devDependencies": {
56
+ "@types/jsonwebtoken": "^9",
57
+ "@types/passport": "^1",
58
+ "@types/passport-jwt": "^4",
59
+ "@types/passport-local": "^1"
60
+ }
61
+ }
@@ -0,0 +1,94 @@
1
+ import { Router } from "express";
2
+ import passport from "passport";
3
+ import type { BackendCtx } from "@tulipes/core/boot";
4
+ import { lazy } from "@app/core";
5
+
6
+ import { AuthController } from "../controllers/auth.controllers.ts";
7
+ import { authenticate, configurePassport } from "../helpers/passport.ts";
8
+ import {
9
+ credentials,
10
+ newPassword,
11
+ refreshBody,
12
+ registration,
13
+ sidParam,
14
+ tokenPair,
15
+ } from "../schemas/auth.schemas.ts";
16
+
17
+ export default function authRoutes(ctx: BackendCtx): void {
18
+ const { app, rai, routes, config } = ctx;
19
+ const auth = new AuthController(ctx);
20
+
21
+ // Passport reads the signing keys when it is configured, so it is
22
+ // configured on the first request, after which every request is identified
23
+ // before any feature route runs.
24
+ app.use(lazy(() => {
25
+ configurePassport(ctx);
26
+ return passport.initialize();
27
+ }));
28
+ app.use(lazy(() => authenticate()));
29
+
30
+ const router = Router();
31
+
32
+ router.post("/register", rai({
33
+ id: "auth:register",
34
+ status: 201,
35
+ name: "Register",
36
+ description: "Create an account and its credential",
37
+ body: registration,
38
+ }), auth.register());
39
+
40
+ router.post("/login", rai({
41
+ id: "auth:login",
42
+ name: "Log in",
43
+ description: "Exchange email and password for a token pair",
44
+ body: credentials,
45
+ returns: tokenPair,
46
+ }), auth.login());
47
+
48
+ router.post("/refresh", rai({
49
+ id: "auth:refresh",
50
+ name: "Refresh tokens",
51
+ description: "Rotate a refresh token for a new pair",
52
+ body: refreshBody,
53
+ returns: tokenPair,
54
+ }), auth.refresh());
55
+
56
+ router.post("/logout", rai({
57
+ id: "auth:logout",
58
+ name: "Log out",
59
+ description: "Revoke this session immediately",
60
+ }), auth.logout());
61
+
62
+ router.get("/me", rai({
63
+ id: "auth:me",
64
+ name: "Who am I",
65
+ description: "The caller and the session behind this request",
66
+ }), auth.me());
67
+
68
+ router.get("/sessions", rai({
69
+ id: "auth:sessions",
70
+ name: "List sessions",
71
+ description: "Every device this account is signed in on",
72
+ // These two are auth's, but they answer a different question from
73
+ // signing in — `folder` puts them under their own heading in the
74
+ // generated collection and spec.
75
+ folder: "sessions",
76
+ }), auth.sessions());
77
+
78
+ router.delete("/sessions/:sid", rai({
79
+ id: "auth:session-end",
80
+ name: "End a session",
81
+ description: "Sign one other device out",
82
+ folder: "sessions",
83
+ params: sidParam,
84
+ }), auth.revokeSession());
85
+
86
+ router.post("/password", rai({
87
+ id: "auth:password",
88
+ name: "Change password",
89
+ description: "Set a new password and end every other session",
90
+ body: newPassword,
91
+ }), auth.changePassword());
92
+
93
+ routes.mount(router, `${config.api!.prefix}/auth`);
94
+ }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod/v4";
2
+
3
+ /**
4
+ * Request shapes for the auth endpoints, used twice from one declaration:
5
+ * `rai()` validates against them, and `tulipes spec` describes them.
6
+ *
7
+ * Messages are i18n keys — the error handler translates them in this
8
+ * module's namespace, so a rejected field speaks the caller's language.
9
+ */
10
+ export const credentials = z.object({
11
+ email: z.email("emailInvalid"),
12
+ password: z.string("passwordRequired").min(1, "passwordRequired"),
13
+ });
14
+
15
+ export const registration = z.object({
16
+ email: z.email("emailInvalid"),
17
+ password: z.string("passwordTooShort").min(8, "passwordTooShort"),
18
+ });
19
+
20
+ // Refresh credentials come from the JSON body, never ambient browser cookies.
21
+ export const refreshBody = z.object({
22
+ refresh_token: z.string("refreshRequired").min(1, "refreshRequired"),
23
+ });
24
+
25
+ export const newPassword = z.object({
26
+ password: z.string("passwordTooShort").min(8, "passwordTooShort"),
27
+ });
28
+
29
+ export const sidParam = z.object({
30
+ sid: z.uuid("noSuchSession"),
31
+ });
32
+
33
+ export const tokenPair = z.object({
34
+ access_token: z.string(),
35
+ refresh_token: z.string(),
36
+ expires_in: z.string(),
37
+ });