@stravigor/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +45 -0
  2. package/package.json +83 -0
  3. package/src/auth/access_token.ts +122 -0
  4. package/src/auth/auth.ts +86 -0
  5. package/src/auth/index.ts +7 -0
  6. package/src/auth/middleware/authenticate.ts +64 -0
  7. package/src/auth/middleware/csrf.ts +62 -0
  8. package/src/auth/middleware/guest.ts +46 -0
  9. package/src/broadcast/broadcast_manager.ts +411 -0
  10. package/src/broadcast/client.ts +302 -0
  11. package/src/broadcast/index.ts +58 -0
  12. package/src/cache/cache_manager.ts +56 -0
  13. package/src/cache/cache_store.ts +31 -0
  14. package/src/cache/helpers.ts +74 -0
  15. package/src/cache/http_cache.ts +109 -0
  16. package/src/cache/index.ts +6 -0
  17. package/src/cache/memory_store.ts +63 -0
  18. package/src/cli/bootstrap.ts +37 -0
  19. package/src/cli/commands/generate_api.ts +74 -0
  20. package/src/cli/commands/generate_key.ts +46 -0
  21. package/src/cli/commands/generate_models.ts +48 -0
  22. package/src/cli/commands/migration_compare.ts +152 -0
  23. package/src/cli/commands/migration_fresh.ts +123 -0
  24. package/src/cli/commands/migration_generate.ts +79 -0
  25. package/src/cli/commands/migration_rollback.ts +53 -0
  26. package/src/cli/commands/migration_run.ts +44 -0
  27. package/src/cli/commands/queue_flush.ts +35 -0
  28. package/src/cli/commands/queue_retry.ts +34 -0
  29. package/src/cli/commands/queue_work.ts +40 -0
  30. package/src/cli/commands/scheduler_work.ts +45 -0
  31. package/src/cli/strav.ts +33 -0
  32. package/src/config/configuration.ts +105 -0
  33. package/src/config/loaders/base_loader.ts +69 -0
  34. package/src/config/loaders/env_loader.ts +112 -0
  35. package/src/config/loaders/typescript_loader.ts +56 -0
  36. package/src/config/types.ts +8 -0
  37. package/src/core/application.ts +4 -0
  38. package/src/core/container.ts +117 -0
  39. package/src/core/index.ts +3 -0
  40. package/src/core/inject.ts +39 -0
  41. package/src/database/database.ts +54 -0
  42. package/src/database/index.ts +30 -0
  43. package/src/database/introspector.ts +446 -0
  44. package/src/database/migration/differ.ts +308 -0
  45. package/src/database/migration/file_generator.ts +125 -0
  46. package/src/database/migration/index.ts +18 -0
  47. package/src/database/migration/runner.ts +133 -0
  48. package/src/database/migration/sql_generator.ts +378 -0
  49. package/src/database/migration/tracker.ts +76 -0
  50. package/src/database/migration/types.ts +189 -0
  51. package/src/database/query_builder.ts +474 -0
  52. package/src/encryption/encryption_manager.ts +209 -0
  53. package/src/encryption/helpers.ts +158 -0
  54. package/src/encryption/index.ts +3 -0
  55. package/src/encryption/types.ts +6 -0
  56. package/src/events/emitter.ts +101 -0
  57. package/src/events/index.ts +2 -0
  58. package/src/exceptions/errors.ts +75 -0
  59. package/src/exceptions/exception_handler.ts +126 -0
  60. package/src/exceptions/helpers.ts +25 -0
  61. package/src/exceptions/http_exception.ts +129 -0
  62. package/src/exceptions/index.ts +23 -0
  63. package/src/exceptions/strav_error.ts +11 -0
  64. package/src/generators/api_generator.ts +972 -0
  65. package/src/generators/config.ts +87 -0
  66. package/src/generators/doc_generator.ts +974 -0
  67. package/src/generators/index.ts +11 -0
  68. package/src/generators/model_generator.ts +586 -0
  69. package/src/generators/route_generator.ts +188 -0
  70. package/src/generators/test_generator.ts +1666 -0
  71. package/src/helpers/crypto.ts +4 -0
  72. package/src/helpers/env.ts +50 -0
  73. package/src/helpers/identity.ts +12 -0
  74. package/src/helpers/index.ts +4 -0
  75. package/src/helpers/strings.ts +67 -0
  76. package/src/http/context.ts +215 -0
  77. package/src/http/cookie.ts +59 -0
  78. package/src/http/cors.ts +163 -0
  79. package/src/http/index.ts +16 -0
  80. package/src/http/middleware.ts +39 -0
  81. package/src/http/rate_limit.ts +173 -0
  82. package/src/http/router.ts +556 -0
  83. package/src/http/server.ts +79 -0
  84. package/src/i18n/defaults/en/validation.json +20 -0
  85. package/src/i18n/helpers.ts +72 -0
  86. package/src/i18n/i18n_manager.ts +155 -0
  87. package/src/i18n/index.ts +4 -0
  88. package/src/i18n/middleware.ts +90 -0
  89. package/src/i18n/translator.ts +96 -0
  90. package/src/i18n/types.ts +17 -0
  91. package/src/logger/index.ts +6 -0
  92. package/src/logger/logger.ts +100 -0
  93. package/src/logger/request_logger.ts +19 -0
  94. package/src/logger/sinks/console_sink.ts +24 -0
  95. package/src/logger/sinks/file_sink.ts +24 -0
  96. package/src/logger/sinks/sink.ts +36 -0
  97. package/src/mail/css_inliner.ts +79 -0
  98. package/src/mail/helpers.ts +212 -0
  99. package/src/mail/index.ts +19 -0
  100. package/src/mail/mail_manager.ts +92 -0
  101. package/src/mail/transports/log_transport.ts +69 -0
  102. package/src/mail/transports/resend_transport.ts +59 -0
  103. package/src/mail/transports/sendgrid_transport.ts +77 -0
  104. package/src/mail/transports/smtp_transport.ts +48 -0
  105. package/src/mail/types.ts +80 -0
  106. package/src/notification/base_notification.ts +67 -0
  107. package/src/notification/channels/database_channel.ts +30 -0
  108. package/src/notification/channels/discord_channel.ts +43 -0
  109. package/src/notification/channels/email_channel.ts +37 -0
  110. package/src/notification/channels/webhook_channel.ts +45 -0
  111. package/src/notification/helpers.ts +214 -0
  112. package/src/notification/index.ts +20 -0
  113. package/src/notification/notification_manager.ts +126 -0
  114. package/src/notification/types.ts +122 -0
  115. package/src/orm/base_model.ts +351 -0
  116. package/src/orm/decorators.ts +127 -0
  117. package/src/orm/index.ts +4 -0
  118. package/src/policy/authorize.ts +44 -0
  119. package/src/policy/index.ts +3 -0
  120. package/src/policy/policy_result.ts +13 -0
  121. package/src/queue/index.ts +11 -0
  122. package/src/queue/queue.ts +338 -0
  123. package/src/queue/worker.ts +197 -0
  124. package/src/scheduler/cron.ts +140 -0
  125. package/src/scheduler/index.ts +7 -0
  126. package/src/scheduler/runner.ts +116 -0
  127. package/src/scheduler/schedule.ts +183 -0
  128. package/src/scheduler/scheduler.ts +47 -0
  129. package/src/schema/database_representation.ts +122 -0
  130. package/src/schema/define_association.ts +60 -0
  131. package/src/schema/define_schema.ts +46 -0
  132. package/src/schema/field_builder.ts +155 -0
  133. package/src/schema/field_definition.ts +66 -0
  134. package/src/schema/index.ts +21 -0
  135. package/src/schema/naming.ts +19 -0
  136. package/src/schema/postgres.ts +109 -0
  137. package/src/schema/registry.ts +157 -0
  138. package/src/schema/representation_builder.ts +479 -0
  139. package/src/schema/type_builder.ts +107 -0
  140. package/src/schema/types.ts +35 -0
  141. package/src/session/index.ts +4 -0
  142. package/src/session/middleware.ts +46 -0
  143. package/src/session/session.ts +308 -0
  144. package/src/session/session_manager.ts +81 -0
  145. package/src/storage/index.ts +13 -0
  146. package/src/storage/local_driver.ts +46 -0
  147. package/src/storage/s3_driver.ts +51 -0
  148. package/src/storage/storage.ts +43 -0
  149. package/src/storage/storage_manager.ts +59 -0
  150. package/src/storage/types.ts +42 -0
  151. package/src/storage/upload.ts +91 -0
  152. package/src/validation/index.ts +18 -0
  153. package/src/validation/rules.ts +170 -0
  154. package/src/validation/validate.ts +41 -0
  155. package/src/view/cache.ts +47 -0
  156. package/src/view/client/islands.ts +50 -0
  157. package/src/view/compiler.ts +185 -0
  158. package/src/view/engine.ts +139 -0
  159. package/src/view/escape.ts +14 -0
  160. package/src/view/index.ts +13 -0
  161. package/src/view/islands/island_builder.ts +161 -0
  162. package/src/view/islands/vue_plugin.ts +140 -0
  163. package/src/view/middleware/static.ts +35 -0
  164. package/src/view/tokenizer.ts +172 -0
  165. package/tsconfig.json +4 -0
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @stravigor/core
2
+
3
+ The Strav framework for Bun. A full-stack TypeScript framework built on [Bun](https://bun.sh) with PostgreSQL.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @stravigor/core
9
+ ```
10
+
11
+ ## Modules
12
+
13
+ Import what you need via subpath exports:
14
+
15
+ ```ts
16
+ import { router } from '@stravigor/core/http'
17
+ import { defineSchema } from '@stravigor/core/schema'
18
+ import BaseModel from '@stravigor/core/orm/base_model'
19
+ import { session } from '@stravigor/core/session'
20
+ import { auth } from '@stravigor/core/auth'
21
+ import { cache } from '@stravigor/core/cache'
22
+ import { mail } from '@stravigor/core/mail'
23
+ import { broadcast } from '@stravigor/core/broadcast'
24
+ import { env } from '@stravigor/core/helpers/env'
25
+ ```
26
+
27
+ Available modules: `core`, `config`, `database`, `schema`, `orm`, `http`, `view`, `validation`, `session`, `auth`, `events`, `queue`, `policy`, `helpers`, `generators`, `logger`, `storage`, `cache`, `scheduler`, `mail`, `notification`, `broadcast`, `encryption`, `exceptions`, `i18n`, `cli`.
28
+
29
+ ## CLI
30
+
31
+ ```bash
32
+ bunx strav migration:run
33
+ bunx strav migration:generate
34
+ bunx strav generate:models
35
+ bunx strav generate:api
36
+ ```
37
+
38
+ ## Related Packages
39
+
40
+ - [@stravigor/ai](https://www.npmjs.com/package/@stravigor/ai) - AI module (Anthropic, OpenAI)
41
+ - [@stravigor/testing](https://www.npmjs.com/package/@stravigor/testing) - Testing utilities
42
+
43
+ ## License
44
+
45
+ MIT
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@stravigor/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "The Strav framework for Bun",
6
+ "license": "MIT",
7
+ "keywords": ["bun", "framework", "typescript", "strav"],
8
+ "files": ["src/", "package.json", "tsconfig.json"],
9
+ "exports": {
10
+ "./core": "./src/core/index.ts",
11
+ "./core/*": "./src/core/*.ts",
12
+ "./config": "./src/config/index.ts",
13
+ "./config/*": "./src/config/*.ts",
14
+ "./database": "./src/database/index.ts",
15
+ "./database/*": "./src/database/*.ts",
16
+ "./schema": "./src/schema/index.ts",
17
+ "./schema/*": "./src/schema/*.ts",
18
+ "./orm": "./src/orm/index.ts",
19
+ "./orm/*": "./src/orm/*.ts",
20
+ "./http": "./src/http/index.ts",
21
+ "./http/*": "./src/http/*.ts",
22
+ "./view": "./src/view/index.ts",
23
+ "./view/*": "./src/view/*.ts",
24
+ "./validation": "./src/validation/index.ts",
25
+ "./validation/*": "./src/validation/*.ts",
26
+ "./session": "./src/session/index.ts",
27
+ "./session/*": "./src/session/*.ts",
28
+ "./auth": "./src/auth/index.ts",
29
+ "./auth/*": "./src/auth/*.ts",
30
+ "./events": "./src/events/index.ts",
31
+ "./events/*": "./src/events/*.ts",
32
+ "./queue": "./src/queue/index.ts",
33
+ "./queue/*": "./src/queue/*.ts",
34
+ "./policy": "./src/policy/index.ts",
35
+ "./policy/*": "./src/policy/*.ts",
36
+ "./helpers": "./src/helpers/index.ts",
37
+ "./helpers/*": "./src/helpers/*.ts",
38
+ "./generators": "./src/generators/index.ts",
39
+ "./generators/*": "./src/generators/*.ts",
40
+ "./logger": "./src/logger/index.ts",
41
+ "./logger/*": "./src/logger/*.ts",
42
+ "./storage": "./src/storage/index.ts",
43
+ "./storage/*": "./src/storage/*.ts",
44
+ "./cache": "./src/cache/index.ts",
45
+ "./cache/*": "./src/cache/*.ts",
46
+ "./scheduler": "./src/scheduler/index.ts",
47
+ "./scheduler/*": "./src/scheduler/*.ts",
48
+ "./mail": "./src/mail/index.ts",
49
+ "./mail/*": "./src/mail/*.ts",
50
+ "./notification": "./src/notification/index.ts",
51
+ "./notification/*": "./src/notification/*.ts",
52
+ "./broadcast": "./src/broadcast/index.ts",
53
+ "./broadcast/*": "./src/broadcast/*.ts",
54
+ "./encryption": "./src/encryption/index.ts",
55
+ "./encryption/*": "./src/encryption/*.ts",
56
+ "./exceptions": "./src/exceptions/index.ts",
57
+ "./exceptions/*": "./src/exceptions/*.ts",
58
+ "./i18n": "./src/i18n/index.ts",
59
+ "./i18n/*": "./src/i18n/*.ts",
60
+ "./cli": "./src/cli/strav.ts",
61
+ "./cli/*": "./src/cli/*.ts"
62
+ },
63
+ "bin": {
64
+ "strav": "./src/cli/strav.ts"
65
+ },
66
+ "dependencies": {
67
+ "@types/luxon": "^3.7.1",
68
+ "chalk": "^5.6.2",
69
+ "commander": "^14.0.3",
70
+ "luxon": "^3.7.2",
71
+ "pino": "^10.3.1",
72
+ "pino-pretty": "^13.1.3",
73
+ "nodemailer": "^6.10.0",
74
+ "@types/nodemailer": "^6.4.0",
75
+ "juice": "^11.0.0",
76
+ "reflect-metadata": "^0.2.2",
77
+ "@vue/compiler-sfc": "^3.5.28"
78
+ },
79
+ "scripts": {
80
+ "test": "bun test tests/",
81
+ "typecheck": "tsc --noEmit"
82
+ }
83
+ }
@@ -0,0 +1,122 @@
1
+ import Auth, { extractUserId, randomHex } from './auth.ts'
2
+
3
+ /** The DB record for an access token (never contains the plain token). */
4
+ export interface AccessTokenData {
5
+ id: number
6
+ userId: string
7
+ name: string
8
+ lastUsedAt: Date | null
9
+ expiresAt: Date | null
10
+ createdAt: Date
11
+ }
12
+
13
+ function hashToken(plain: string): string {
14
+ return new Bun.CryptoHasher('sha256').update(plain).digest('hex')
15
+ }
16
+
17
+ /**
18
+ * Opaque, SHA-256-hashed access tokens stored in the database.
19
+ *
20
+ * The plain-text token is returned exactly once at creation time and
21
+ * is never stored. Even if the database leaks, tokens cannot be recovered.
22
+ *
23
+ * @example
24
+ * // Create
25
+ * const { token, accessToken } = await AccessToken.create(user, 'mobile-app')
26
+ * // token = 'a1b2c3...' (give to the client, shown once)
27
+ *
28
+ * // Validate (used internally by the auth middleware)
29
+ * const record = await AccessToken.validate(plainToken)
30
+ *
31
+ * // Revoke
32
+ * await AccessToken.revoke(accessToken.id)
33
+ */
34
+ export default class AccessToken {
35
+ /**
36
+ * Generate a new access token for the given user.
37
+ * Returns the plain-text token (shown once) and the database record.
38
+ */
39
+ static async create(
40
+ user: unknown,
41
+ name: string
42
+ ): Promise<{ token: string; accessToken: AccessTokenData }> {
43
+ const userId = extractUserId(user)
44
+ const plain = randomHex(32) // 64-char hex string
45
+ const hash = hashToken(plain)
46
+
47
+ const expCfg = Auth.config.token.expiration
48
+ const expiresAt = expCfg ? new Date(Date.now() + expCfg * 60_000) : null
49
+
50
+ const rows = await Auth.db.sql`
51
+ INSERT INTO "_strav_access_tokens" ("user_id", "name", "token", "expires_at")
52
+ VALUES (${userId}, ${name}, ${hash}, ${expiresAt})
53
+ RETURNING *
54
+ `
55
+
56
+ return {
57
+ token: plain,
58
+ accessToken: AccessToken.hydrate(rows[0] as Record<string, unknown>),
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Validate a plain-text token. Returns the token record if valid, null otherwise.
64
+ * Automatically rejects expired tokens and updates last_used_at.
65
+ */
66
+ static async validate(plainToken: string): Promise<AccessTokenData | null> {
67
+ const hash = hashToken(plainToken)
68
+
69
+ const rows = await Auth.db.sql`
70
+ SELECT * FROM "_strav_access_tokens" WHERE "token" = ${hash} LIMIT 1
71
+ `
72
+ if (rows.length === 0) return null
73
+
74
+ const record = AccessToken.hydrate(rows[0] as Record<string, unknown>)
75
+
76
+ if (record.expiresAt && record.expiresAt.getTime() < Date.now()) {
77
+ return null
78
+ }
79
+
80
+ // Update last_used_at (fire-and-forget)
81
+ Auth.db.sql`
82
+ UPDATE "_strav_access_tokens"
83
+ SET "last_used_at" = NOW()
84
+ WHERE "id" = ${record.id}
85
+ `.then(
86
+ () => {},
87
+ () => {}
88
+ )
89
+
90
+ return record
91
+ }
92
+
93
+ /** Revoke (delete) a single token by its database ID. */
94
+ static async revoke(id: number): Promise<void> {
95
+ await Auth.db.sql`
96
+ DELETE FROM "_strav_access_tokens" WHERE "id" = ${id}
97
+ `
98
+ }
99
+
100
+ /** Revoke all tokens belonging to a user. */
101
+ static async revokeAllFor(user: unknown): Promise<void> {
102
+ const userId = extractUserId(user)
103
+ await Auth.db.sql`
104
+ DELETE FROM "_strav_access_tokens" WHERE "user_id" = ${userId}
105
+ `
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Internal
110
+ // ---------------------------------------------------------------------------
111
+
112
+ private static hydrate(row: Record<string, unknown>): AccessTokenData {
113
+ return {
114
+ id: row.id as number,
115
+ userId: row.user_id as string,
116
+ name: row.name as string,
117
+ lastUsedAt: (row.last_used_at as Date) ?? null,
118
+ expiresAt: (row.expires_at as Date) ?? null,
119
+ createdAt: row.created_at as Date,
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,86 @@
1
+ import { inject } from '../core/inject.ts'
2
+ import { ConfigurationError } from '../exceptions/errors.ts'
3
+ import Configuration from '../config/configuration.ts'
4
+ import Database from '../database/database.ts'
5
+
6
+ // Re-export helpers that were originally defined here
7
+ export { extractUserId } from '../helpers/identity.ts'
8
+ export { randomHex } from '../helpers/crypto.ts'
9
+
10
+ export interface TokenConfig {
11
+ expiration: number | null
12
+ }
13
+
14
+ export interface AuthConfig {
15
+ default: string
16
+ token: TokenConfig
17
+ }
18
+
19
+ type UserResolver = (id: string | number) => Promise<unknown>
20
+
21
+ /**
22
+ * Central auth configuration hub.
23
+ *
24
+ * Resolved once via the DI container — stores the database reference,
25
+ * parsed config, and user resolver for AccessToken and auth middleware.
26
+ *
27
+ * @example
28
+ * app.singleton(Auth)
29
+ * app.resolve(Auth)
30
+ * Auth.useResolver((id) => User.find(id))
31
+ * await Auth.ensureTables()
32
+ */
33
+ @inject
34
+ export default class Auth {
35
+ private static _db: Database
36
+ private static _config: AuthConfig
37
+ private static _resolver: UserResolver
38
+
39
+ constructor(db: Database, config: Configuration) {
40
+ Auth._db = db
41
+ Auth._config = {
42
+ default: config.get('auth.default', 'session') as string,
43
+ token: {
44
+ expiration: null,
45
+ ...(config.get('auth.token', {}) as object),
46
+ },
47
+ }
48
+ }
49
+
50
+ /** Register the function used to load a user by ID. */
51
+ static useResolver(resolver: UserResolver): void {
52
+ Auth._resolver = resolver
53
+ }
54
+
55
+ static get db(): Database {
56
+ if (!Auth._db) throw new ConfigurationError('Auth not configured. Resolve Auth through the container first.')
57
+ return Auth._db
58
+ }
59
+
60
+ static get config(): AuthConfig {
61
+ return Auth._config
62
+ }
63
+
64
+ /** Load a user by ID using the registered resolver. */
65
+ static async resolveUser(id: string | number): Promise<unknown> {
66
+ if (!Auth._resolver) {
67
+ throw new ConfigurationError('Auth resolver not configured. Call Auth.useResolver() first.')
68
+ }
69
+ return Auth._resolver(id)
70
+ }
71
+
72
+ /** Create the internal access_tokens table if it doesn't exist. */
73
+ static async ensureTables(): Promise<void> {
74
+ await Auth.db.sql`
75
+ CREATE TABLE IF NOT EXISTS "_strav_access_tokens" (
76
+ "id" SERIAL PRIMARY KEY,
77
+ "user_id" VARCHAR(255) NOT NULL,
78
+ "name" VARCHAR(255) NOT NULL,
79
+ "token" VARCHAR(64) NOT NULL UNIQUE,
80
+ "last_used_at" TIMESTAMPTZ,
81
+ "expires_at" TIMESTAMPTZ,
82
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
83
+ )
84
+ `
85
+ }
86
+ }
@@ -0,0 +1,7 @@
1
+ export { default as Auth } from './auth.ts'
2
+ export { default as AccessToken } from './access_token.ts'
3
+ export { auth } from './middleware/authenticate.ts'
4
+ export { csrf } from './middleware/csrf.ts'
5
+ export { guest } from './middleware/guest.ts'
6
+ export type { AuthConfig, TokenConfig } from './auth.ts'
7
+ export type { AccessTokenData } from './access_token.ts'
@@ -0,0 +1,64 @@
1
+ import type { Middleware } from '../../http/middleware.ts'
2
+ import Auth from '../auth.ts'
3
+ import type Session from '../../session/session.ts'
4
+ import AccessToken from '../access_token.ts'
5
+
6
+ /**
7
+ * Require the request to be authenticated.
8
+ *
9
+ * For the session guard, requires the `session()` middleware to run first
10
+ * so that `ctx.get('session')` is available. Checks that the session has
11
+ * a user associated with it.
12
+ *
13
+ * Sets:
14
+ * - `ctx.get('user')` — the resolved user object
15
+ * - `ctx.get('accessToken')` — the AccessTokenData (token guard only)
16
+ *
17
+ * @param guard 'session' | 'token' (defaults to config `auth.default`)
18
+ *
19
+ * @example
20
+ * router.group({ middleware: [session(), auth()] }, (r) => { ... })
21
+ * router.group({ middleware: [auth('token')] }, (r) => { ... })
22
+ */
23
+ export function auth(guard?: string): Middleware {
24
+ return async (ctx, next) => {
25
+ const guardName = guard ?? Auth.config.default
26
+
27
+ if (guardName === 'session') {
28
+ const session = ctx.get<Session>('session')
29
+
30
+ if (!session || !session.isAuthenticated || session.isExpired()) {
31
+ return ctx.json({ error: 'Unauthenticated' }, 401)
32
+ }
33
+
34
+ const user = await Auth.resolveUser(session.userId!)
35
+ if (!user) return ctx.json({ error: 'Unauthenticated' }, 401)
36
+
37
+ ctx.set('user', user)
38
+
39
+ const response = await next()
40
+ await session.touch()
41
+ return response
42
+ }
43
+
44
+ if (guardName === 'token') {
45
+ const header = ctx.header('authorization')
46
+ if (!header?.startsWith('Bearer ')) {
47
+ return ctx.json({ error: 'Unauthenticated' }, 401)
48
+ }
49
+
50
+ const accessToken = await AccessToken.validate(header.slice(7))
51
+ if (!accessToken) return ctx.json({ error: 'Unauthenticated' }, 401)
52
+
53
+ const user = await Auth.resolveUser(accessToken.userId)
54
+ if (!user) return ctx.json({ error: 'Unauthenticated' }, 401)
55
+
56
+ ctx.set('user', user)
57
+ ctx.set('accessToken', accessToken)
58
+
59
+ return next()
60
+ }
61
+
62
+ return ctx.json({ error: `Unknown auth guard: ${guardName}` }, 500)
63
+ }
64
+ }
@@ -0,0 +1,62 @@
1
+ import type { Middleware } from '../../http/middleware.ts'
2
+ import type Session from '../../session/session.ts'
3
+
4
+ /**
5
+ * CSRF protection middleware.
6
+ *
7
+ * Must be placed **after** the `session()` middleware so that
8
+ * `ctx.get('session')` is available. Works for both anonymous and
9
+ * authenticated sessions.
10
+ *
11
+ * On safe methods (GET, HEAD, OPTIONS) the CSRF token is made available
12
+ * via `ctx.get('csrfToken')` for embedding in forms or meta tags.
13
+ *
14
+ * On state-changing methods, the middleware checks for a matching token in:
15
+ * 1. `X-CSRF-Token` header
16
+ * 2. `X-XSRF-Token` header
17
+ * 3. `_token` field in a JSON or form body
18
+ *
19
+ * @example
20
+ * router.group({ middleware: [session(), csrf()] }, (r) => {
21
+ * r.post('/login', handleLogin)
22
+ * })
23
+ */
24
+ export function csrf(): Middleware {
25
+ return async (ctx, next) => {
26
+ const session = ctx.get<Session>('session')
27
+
28
+ if (['GET', 'HEAD', 'OPTIONS'].includes(ctx.method)) {
29
+ if (session) ctx.set('csrfToken', session.csrfToken)
30
+ return next()
31
+ }
32
+
33
+ if (!session) {
34
+ return ctx.json({ error: 'Session required for CSRF protection' }, 403)
35
+ }
36
+
37
+ // Check headers first
38
+ let token = ctx.header('X-CSRF-Token') ?? ctx.header('X-XSRF-Token')
39
+
40
+ // Fall back to request body
41
+ if (!token) {
42
+ const contentType = ctx.header('content-type') ?? ''
43
+
44
+ if (contentType.includes('application/json')) {
45
+ const body = await ctx.body<Record<string, unknown>>()
46
+ if (typeof body._token === 'string') token = body._token
47
+ } else if (
48
+ contentType.includes('application/x-www-form-urlencoded') ||
49
+ contentType.includes('multipart/form-data')
50
+ ) {
51
+ const body = await ctx.body<FormData>()
52
+ if (body instanceof FormData) token = body.get('_token') as string | null
53
+ }
54
+ }
55
+
56
+ if (!token || token !== session.csrfToken) {
57
+ return ctx.json({ error: 'CSRF token mismatch' }, 403)
58
+ }
59
+
60
+ return next()
61
+ }
62
+ }
@@ -0,0 +1,46 @@
1
+ import type { Middleware } from '../../http/middleware.ts'
2
+ import Auth from '../auth.ts'
3
+ import type Session from '../../session/session.ts'
4
+ import AccessToken from '../access_token.ts'
5
+
6
+ /**
7
+ * Only allow unauthenticated requests through.
8
+ *
9
+ * For the session guard, requires the `session()` middleware to run first
10
+ * so that `ctx.get('session')` is available.
11
+ *
12
+ * Useful for login/register pages that should not be accessible to
13
+ * users who are already logged in.
14
+ *
15
+ * @param redirectTo If provided, authenticated users are redirected here
16
+ * instead of receiving a 403.
17
+ *
18
+ * @example
19
+ * router.group({ middleware: [session(), guest('/dashboard')] }, (r) => {
20
+ * r.get('/login', showLoginPage)
21
+ * })
22
+ */
23
+ export function guest(redirectTo?: string): Middleware {
24
+ return async (ctx, next) => {
25
+ const guardName = Auth.config.default
26
+ let isAuthenticated = false
27
+
28
+ if (guardName === 'session') {
29
+ const session = ctx.get<Session>('session')
30
+ isAuthenticated = session !== null && session.isAuthenticated && !session.isExpired()
31
+ } else if (guardName === 'token') {
32
+ const header = ctx.header('authorization')
33
+ if (header?.startsWith('Bearer ')) {
34
+ const accessToken = await AccessToken.validate(header.slice(7))
35
+ isAuthenticated = accessToken !== null
36
+ }
37
+ }
38
+
39
+ if (isAuthenticated) {
40
+ if (redirectTo) return ctx.redirect(redirectTo)
41
+ return ctx.json({ error: 'Already authenticated' }, 403)
42
+ }
43
+
44
+ return next()
45
+ }
46
+ }