@oktis-works/api 0.0.0-stage → 0.1.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 (154) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +170 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/middleware/auth.d.ts +8 -0
  6. package/dist/middleware/auth.d.ts.map +1 -0
  7. package/dist/middleware/auth.js +32 -0
  8. package/dist/middleware/auth.js.map +1 -0
  9. package/dist/middleware/csrf.d.ts +13 -0
  10. package/dist/middleware/csrf.d.ts.map +1 -0
  11. package/dist/middleware/csrf.js +64 -0
  12. package/dist/middleware/csrf.js.map +1 -0
  13. package/dist/middleware/csrf.test.d.ts +2 -0
  14. package/dist/middleware/csrf.test.d.ts.map +1 -0
  15. package/dist/middleware/csrf.test.js +94 -0
  16. package/dist/middleware/csrf.test.js.map +1 -0
  17. package/dist/middleware/rate-limit.d.ts +18 -0
  18. package/dist/middleware/rate-limit.d.ts.map +1 -0
  19. package/dist/middleware/rate-limit.js +58 -0
  20. package/dist/middleware/rate-limit.js.map +1 -0
  21. package/dist/middleware/rate-limit.test.d.ts +2 -0
  22. package/dist/middleware/rate-limit.test.d.ts.map +1 -0
  23. package/dist/middleware/rate-limit.test.js +58 -0
  24. package/dist/middleware/rate-limit.test.js.map +1 -0
  25. package/dist/middleware/trace.d.ts +7 -0
  26. package/dist/middleware/trace.d.ts.map +1 -0
  27. package/dist/middleware/trace.js +14 -0
  28. package/dist/middleware/trace.js.map +1 -0
  29. package/dist/middleware/trace.test.d.ts +2 -0
  30. package/dist/middleware/trace.test.d.ts.map +1 -0
  31. package/dist/middleware/trace.test.js +27 -0
  32. package/dist/middleware/trace.test.js.map +1 -0
  33. package/dist/routes/audit-logs/index.d.ts +4 -0
  34. package/dist/routes/audit-logs/index.d.ts.map +1 -0
  35. package/dist/routes/audit-logs/index.js +19 -0
  36. package/dist/routes/audit-logs/index.js.map +1 -0
  37. package/dist/routes/auth/index.d.ts +4 -0
  38. package/dist/routes/auth/index.d.ts.map +1 -0
  39. package/dist/routes/auth/index.js +84 -0
  40. package/dist/routes/auth/index.js.map +1 -0
  41. package/dist/routes/auth/me.d.ts +4 -0
  42. package/dist/routes/auth/me.d.ts.map +1 -0
  43. package/dist/routes/auth/me.js +16 -0
  44. package/dist/routes/auth/me.js.map +1 -0
  45. package/dist/routes/categories/index.d.ts +4 -0
  46. package/dist/routes/categories/index.d.ts.map +1 -0
  47. package/dist/routes/categories/index.js +53 -0
  48. package/dist/routes/categories/index.js.map +1 -0
  49. package/dist/routes/content/index.d.ts +4 -0
  50. package/dist/routes/content/index.d.ts.map +1 -0
  51. package/dist/routes/content/index.js +83 -0
  52. package/dist/routes/content/index.js.map +1 -0
  53. package/dist/routes/content-types/index.d.ts +4 -0
  54. package/dist/routes/content-types/index.d.ts.map +1 -0
  55. package/dist/routes/content-types/index.js +70 -0
  56. package/dist/routes/content-types/index.js.map +1 -0
  57. package/dist/routes/deployments/index.d.ts +4 -0
  58. package/dist/routes/deployments/index.d.ts.map +1 -0
  59. package/dist/routes/deployments/index.js +52 -0
  60. package/dist/routes/deployments/index.js.map +1 -0
  61. package/dist/routes/docs/index.d.ts +4 -0
  62. package/dist/routes/docs/index.d.ts.map +1 -0
  63. package/dist/routes/docs/index.js +120 -0
  64. package/dist/routes/docs/index.js.map +1 -0
  65. package/dist/routes/events/index.d.ts +4 -0
  66. package/dist/routes/events/index.d.ts.map +1 -0
  67. package/dist/routes/events/index.js +28 -0
  68. package/dist/routes/events/index.js.map +1 -0
  69. package/dist/routes/field-groups/index.d.ts +4 -0
  70. package/dist/routes/field-groups/index.d.ts.map +1 -0
  71. package/dist/routes/field-groups/index.js +77 -0
  72. package/dist/routes/field-groups/index.js.map +1 -0
  73. package/dist/routes/field-types/index.d.ts +4 -0
  74. package/dist/routes/field-types/index.d.ts.map +1 -0
  75. package/dist/routes/field-types/index.js +21 -0
  76. package/dist/routes/field-types/index.js.map +1 -0
  77. package/dist/routes/health/index.d.ts +4 -0
  78. package/dist/routes/health/index.d.ts.map +1 -0
  79. package/dist/routes/health/index.js +32 -0
  80. package/dist/routes/health/index.js.map +1 -0
  81. package/dist/routes/hooks/index.d.ts +4 -0
  82. package/dist/routes/hooks/index.d.ts.map +1 -0
  83. package/dist/routes/hooks/index.js +11 -0
  84. package/dist/routes/hooks/index.js.map +1 -0
  85. package/dist/routes/media/index.d.ts +4 -0
  86. package/dist/routes/media/index.d.ts.map +1 -0
  87. package/dist/routes/media/index.js +51 -0
  88. package/dist/routes/media/index.js.map +1 -0
  89. package/dist/routes/menus/index.d.ts +4 -0
  90. package/dist/routes/menus/index.d.ts.map +1 -0
  91. package/dist/routes/menus/index.js +58 -0
  92. package/dist/routes/menus/index.js.map +1 -0
  93. package/dist/routes/metrics/index.d.ts +4 -0
  94. package/dist/routes/metrics/index.d.ts.map +1 -0
  95. package/dist/routes/metrics/index.js +31 -0
  96. package/dist/routes/metrics/index.js.map +1 -0
  97. package/dist/routes/metrics/index.test.d.ts +2 -0
  98. package/dist/routes/metrics/index.test.d.ts.map +1 -0
  99. package/dist/routes/metrics/index.test.js +64 -0
  100. package/dist/routes/metrics/index.test.js.map +1 -0
  101. package/dist/routes/pages/index.d.ts +3 -0
  102. package/dist/routes/pages/index.d.ts.map +1 -0
  103. package/dist/routes/pages/index.js +6 -0
  104. package/dist/routes/pages/index.js.map +1 -0
  105. package/dist/routes/plugins/index.d.ts +4 -0
  106. package/dist/routes/plugins/index.d.ts.map +1 -0
  107. package/dist/routes/plugins/index.js +65 -0
  108. package/dist/routes/plugins/index.js.map +1 -0
  109. package/dist/routes/posts/index.d.ts +3 -0
  110. package/dist/routes/posts/index.d.ts.map +1 -0
  111. package/dist/routes/posts/index.js +6 -0
  112. package/dist/routes/posts/index.js.map +1 -0
  113. package/dist/routes/posts/revisions.test.d.ts +2 -0
  114. package/dist/routes/posts/revisions.test.d.ts.map +1 -0
  115. package/dist/routes/posts/revisions.test.js +57 -0
  116. package/dist/routes/posts/revisions.test.js.map +1 -0
  117. package/dist/routes/roles/index.d.ts +4 -0
  118. package/dist/routes/roles/index.d.ts.map +1 -0
  119. package/dist/routes/roles/index.js +56 -0
  120. package/dist/routes/roles/index.js.map +1 -0
  121. package/dist/routes/settings/index.d.ts +4 -0
  122. package/dist/routes/settings/index.d.ts.map +1 -0
  123. package/dist/routes/settings/index.js +29 -0
  124. package/dist/routes/settings/index.js.map +1 -0
  125. package/dist/routes/shared/content-collection.d.ts +9 -0
  126. package/dist/routes/shared/content-collection.d.ts.map +1 -0
  127. package/dist/routes/shared/content-collection.js +104 -0
  128. package/dist/routes/shared/content-collection.js.map +1 -0
  129. package/dist/routes/tags/index.d.ts +4 -0
  130. package/dist/routes/tags/index.d.ts.map +1 -0
  131. package/dist/routes/tags/index.js +32 -0
  132. package/dist/routes/tags/index.js.map +1 -0
  133. package/dist/routes/taxonomies/index.d.ts +4 -0
  134. package/dist/routes/taxonomies/index.d.ts.map +1 -0
  135. package/dist/routes/taxonomies/index.js +114 -0
  136. package/dist/routes/taxonomies/index.js.map +1 -0
  137. package/dist/routes/tenants/index.d.ts +4 -0
  138. package/dist/routes/tenants/index.d.ts.map +1 -0
  139. package/dist/routes/tenants/index.js +66 -0
  140. package/dist/routes/tenants/index.js.map +1 -0
  141. package/dist/routes/themes/index.d.ts +4 -0
  142. package/dist/routes/themes/index.d.ts.map +1 -0
  143. package/dist/routes/themes/index.js +54 -0
  144. package/dist/routes/themes/index.js.map +1 -0
  145. package/dist/routes/users/index.d.ts +4 -0
  146. package/dist/routes/users/index.d.ts.map +1 -0
  147. package/dist/routes/users/index.js +66 -0
  148. package/dist/routes/users/index.js.map +1 -0
  149. package/dist/routes/webhooks/index.d.ts +4 -0
  150. package/dist/routes/webhooks/index.d.ts.map +1 -0
  151. package/dist/routes/webhooks/index.js +30 -0
  152. package/dist/routes/webhooks/index.js.map +1 -0
  153. package/package.json +43 -4
  154. package/README.md +0 -3
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env bun
2
+ // @oktis-works/api - Main Entry Point
3
+ import { Hono } from 'hono';
4
+ import { logger } from 'hono/logger';
5
+ import { cors } from 'hono/cors';
6
+ import { prettyJSON } from 'hono/pretty-json';
7
+ import { serve } from '@hono/node-server';
8
+ import { bootstrap, getEventBus, registerEventAuditLog } from '@oktis-works/core';
9
+ import { traceMiddleware } from './middleware/trace.js';
10
+ import { rateLimitMiddleware } from './middleware/rate-limit.js';
11
+ import { csrfMiddleware, issueCsrfToken, CSRF_COOKIE } from './middleware/csrf.js';
12
+ import { establishTenantContext } from '@oktis-works/core';
13
+ import authRouter from './routes/auth/index.js';
14
+ import meRouter from './routes/auth/me.js';
15
+ import contentRouter from './routes/content/index.js';
16
+ import postsRouter from './routes/posts/index.js';
17
+ import pagesRouter from './routes/pages/index.js';
18
+ import healthRouter from './routes/health/index.js';
19
+ import usersRouter from './routes/users/index.js';
20
+ import tenantsRouter from './routes/tenants/index.js';
21
+ import rolesRouter from './routes/roles/index.js';
22
+ import mediaRouter from './routes/media/index.js';
23
+ import categoriesRouter from './routes/categories/index.js';
24
+ import tagsRouter from './routes/tags/index.js';
25
+ import menusRouter from './routes/menus/index.js';
26
+ import settingsRouter from './routes/settings/index.js';
27
+ import pluginsRouter from './routes/plugins/index.js';
28
+ import themesRouter from './routes/themes/index.js';
29
+ import deploymentsRouter from './routes/deployments/index.js';
30
+ import eventsRouter from './routes/events/index.js';
31
+ import webhooksRouter from './routes/webhooks/index.js';
32
+ import contentTypesRouter from './routes/content-types/index.js';
33
+ import taxonomiesRouter from './routes/taxonomies/index.js';
34
+ import fieldGroupsRouter from './routes/field-groups/index.js';
35
+ import fieldTypesRouter from './routes/field-types/index.js';
36
+ import metricsRouter from './routes/metrics/index.js';
37
+ import auditLogsRouter from './routes/audit-logs/index.js';
38
+ import docsRouter from './routes/docs/index.js';
39
+ import hooksCatalogRouter from './routes/hooks/index.js';
40
+ async function main() {
41
+ // Initialize application
42
+ const lifecycle = await bootstrap();
43
+ const app = new Hono();
44
+ // REQ-event-bus-005: eventos de domínio → AuditLog
45
+ registerEventAuditLog(getEventBus());
46
+ // Global middleware
47
+ app.use('*', traceMiddleware);
48
+ app.use('*', logger());
49
+ app.use('*', cors({
50
+ origin: '*',
51
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
52
+ allowHeaders: ['Content-Type', 'Authorization', 'X-Tenant-ID'],
53
+ }));
54
+ app.use('*', prettyJSON());
55
+ // rest-api-003: rate limiting global (429 + Retry-After); probes isentos
56
+ app.use('*', rateLimitMiddleware());
57
+ // security-001: emissão de cookie CSRF + validação double-submit
58
+ app.use('*', async (c, next) => {
59
+ if (!c.req.header('Cookie')?.includes(CSRF_COOKIE)) {
60
+ c.header('Set-Cookie', `${CSRF_COOKIE}=${issueCsrfToken()}; Path=/; SameSite=Lax; Secure`);
61
+ }
62
+ await next();
63
+ });
64
+ app.use('/api/*', csrfMiddleware);
65
+ // Tenant context middleware
66
+ app.use('*', async (c, next) => {
67
+ const tenantId = c.req.header('X-Tenant-ID') ?? c.req.query('tenantId');
68
+ if (tenantId) {
69
+ await establishTenantContext(tenantId);
70
+ }
71
+ await next();
72
+ });
73
+ // Health routes (no auth required)
74
+ app.route('/health', healthRouter);
75
+ // API routes
76
+ app.route('/api/v1/auth', authRouter);
77
+ app.route('/api/v1/auth', meRouter);
78
+ app.route('/api/v1/users', usersRouter);
79
+ app.route('/api/v1/tenants', tenantsRouter);
80
+ app.route('/api/v1/roles', rolesRouter);
81
+ app.route('/api/v1/media', mediaRouter);
82
+ app.route('/api/v1/content', contentRouter);
83
+ app.route('/api/v1/posts', postsRouter);
84
+ app.route('/api/v1/pages', pagesRouter);
85
+ app.route('/api/v1/categories', categoriesRouter);
86
+ app.route('/api/v1/tags', tagsRouter);
87
+ app.route('/api/v1/menus', menusRouter);
88
+ app.route('/api/v1/settings', settingsRouter);
89
+ app.route('/api/v1/plugins', pluginsRouter);
90
+ app.route('/api/v1/themes', themesRouter);
91
+ app.route('/api/v1/deployments', deploymentsRouter);
92
+ app.route('/api/v1/events', eventsRouter);
93
+ app.route('/api/v1/webhooks', webhooksRouter);
94
+ app.route('/api/v1/content-types', contentTypesRouter);
95
+ app.route('/api/v1/taxonomies', taxonomiesRouter);
96
+ app.route('/api/v1/field-groups', fieldGroupsRouter);
97
+ app.route('/api/v1/field-types', fieldTypesRouter);
98
+ app.route('/api/v1/health', healthRouter);
99
+ app.route('/api/v1/metrics', metricsRouter);
100
+ app.route('/api/v1/audit-logs', auditLogsRouter);
101
+ // rest-api-004: documento OpenAPI 3.1
102
+ app.route('/api/v1/docs', docsRouter);
103
+ // REQU-035-002: catálogo de hooks
104
+ app.route('/api/v1/hooks/catalog', hooksCatalogRouter);
105
+ // API info endpoint
106
+ app.get('/api', (c) => {
107
+ return c.json({
108
+ name: 'OkCMS API',
109
+ version: '0.1.1',
110
+ description: 'Hybrid, modular, API-first CMS',
111
+ endpoints: {
112
+ health: '/health',
113
+ auth: '/api/v1/auth',
114
+ users: '/api/v1/users',
115
+ tenants: '/api/v1/tenants',
116
+ roles: '/api/v1/roles',
117
+ media: '/api/v1/media',
118
+ content: '/api/v1/content',
119
+ posts: '/api/v1/posts',
120
+ pages: '/api/v1/pages',
121
+ categories: '/api/v1/categories',
122
+ tags: '/api/v1/tags',
123
+ menus: '/api/v1/menus',
124
+ settings: '/api/v1/settings',
125
+ plugins: '/api/v1/plugins',
126
+ themes: '/api/v1/themes',
127
+ deployments: '/api/v1/deployments',
128
+ events: '/api/v1/events',
129
+ webhooks: '/api/v1/webhooks',
130
+ contentTypes: '/api/v1/content-types',
131
+ taxonomies: '/api/v1/taxonomies',
132
+ fieldGroups: '/api/v1/field-groups',
133
+ fieldTypes: '/api/v1/field-types',
134
+ },
135
+ });
136
+ });
137
+ // 404 handler
138
+ app.notFound((c) => {
139
+ return c.json({ error: 'Not found' }, 404);
140
+ });
141
+ // Error handler
142
+ app.onError((err, c) => {
143
+ console.error('Server error:', err);
144
+ return c.json({ error: 'Internal server error' }, 500);
145
+ });
146
+ // Start server
147
+ const port = Number(process.env['PORT'] ?? 3000);
148
+ serve({
149
+ fetch: app.fetch,
150
+ port,
151
+ }, (info) => {
152
+ console.log(`OkCMS API running on http://localhost:${info.port}`);
153
+ });
154
+ // Graceful shutdown
155
+ process.on('SIGTERM', async () => {
156
+ console.log('SIGTERM received, shutting down...');
157
+ await lifecycle.stop();
158
+ process.exit(0);
159
+ });
160
+ process.on('SIGINT', async () => {
161
+ console.log('SIGINT received, shutting down...');
162
+ await lifecycle.stop();
163
+ process.exit(0);
164
+ });
165
+ }
166
+ main().catch((error) => {
167
+ console.error('Failed to start server:', error);
168
+ process.exit(1);
169
+ });
170
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { Context, Next } from 'hono';
2
+ export declare const authMiddleware: (c: Context, next: Next) => Promise<(Response & import("hono").TypedResponse<{
3
+ error: string;
4
+ }>) | undefined>;
5
+ export declare const requirePermission: (action: string, resource: string) => (c: Context, next: Next) => Promise<(Response & import("hono").TypedResponse<{
6
+ error: string;
7
+ }>) | undefined>;
8
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAO1C,eAAO,MAAM,cAAc,MAAa,OAAO,QAAQ,IAAI;;gBAqB1D,CAAC;AAEF,eAAO,MAAM,iBAAiB,WAAY,MAAM,YAAY,MAAM,SAC/C,OAAO,QAAQ,IAAI;;gBASrC,CAAC"}
@@ -0,0 +1,32 @@
1
+ // @oktis-works/api - Authentication Middleware
2
+ import { AuthService } from '@oktis-works/auth';
3
+ import { loadConfig } from '@oktis-works/config';
4
+ const config = loadConfig();
5
+ const authService = new AuthService(config.auth);
6
+ export const authMiddleware = async (c, next) => {
7
+ const authHeader = c.req.header('Authorization');
8
+ if (!authHeader?.startsWith('Bearer ')) {
9
+ return c.json({ error: 'Missing or invalid authorization header' }, 401);
10
+ }
11
+ const token = authHeader.slice(7);
12
+ const payload = await authService.verifyToken(token);
13
+ if (!payload) {
14
+ return c.json({ error: 'Invalid or expired token' }, 401);
15
+ }
16
+ // Set user context
17
+ c.set('userId', payload.sub);
18
+ c.set('userEmail', payload.email);
19
+ c.set('tenantId', payload.tenantId);
20
+ c.set('userRoles', payload.roles);
21
+ await next();
22
+ };
23
+ export const requirePermission = (action, resource) => {
24
+ return async (c, next) => {
25
+ const userRoles = c.get('userRoles');
26
+ if (!authService.hasPermission(userRoles, action, resource)) {
27
+ return c.json({ error: 'Insufficient permissions' }, 403);
28
+ }
29
+ await next();
30
+ };
31
+ };
32
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAG/C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;AAC5B,MAAM,WAAW,GAAG,IAAI,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAEjD,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EAAE,CAAU,EAAE,IAAU,EAAE,EAAE;IAC7D,MAAM,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAEjD,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACvC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,EAAE,GAAG,CAAC,CAAC;IAC3E,CAAC;IAED,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAErD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,EAAE,GAAG,CAAC,CAAC;IAC5D,CAAC;IAED,mBAAmB;IACnB,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAElC,MAAM,IAAI,EAAE,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,MAAc,EAAE,QAAgB,EAAE,EAAE;IACpE,OAAO,KAAK,EAAE,CAAU,EAAE,IAAU,EAAE,EAAE;QACtC,MAAM,SAAS,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAa,CAAC;QAEjD,IAAI,CAAC,WAAW,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;YAC5D,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,EAAE,GAAG,CAAC,CAAC;QAC5D,CAAC;QAED,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { Context, Next } from 'hono';
2
+ export declare const CSRF_COOKIE = "bl_csrf";
3
+ export declare const CSRF_HEADER = "X-CSRF-Token";
4
+ export declare function issueCsrfToken(): string;
5
+ /** Origem esperada: mesma do Host da requisição (ou configurada em TRUSTED_ORIGINS). */
6
+ export declare function isSameOrigin(c: Context): boolean;
7
+ /**
8
+ * Double-submit cookie: emite cookie bl_csrf para navegadores e valida que
9
+ * header X-CSRF-Token === cookie em requisições state-changing com cookie de sessão.
10
+ * Clientes não-navegador (Authorization Bearer) ficam isentos.
11
+ */
12
+ export declare function csrfMiddleware(c: Context, next: Next): Promise<Response | undefined>;
13
+ //# sourceMappingURL=csrf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf.d.ts","sourceRoot":"","sources":["../../src/middleware/csrf.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK1C,eAAO,MAAM,WAAW,YAAY,CAAC;AACrC,eAAO,MAAM,WAAW,iBAAiB,CAAC;AAE1C,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAoBhD;AAWD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAyB1F"}
@@ -0,0 +1,64 @@
1
+ // @oktis-works/api - CSRF via double-submit cookie + verificação de Origin (security-001)
2
+ import { randomUUID } from 'node:crypto';
3
+ const STATE_CHANGING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
4
+ export const CSRF_COOKIE = 'bl_csrf';
5
+ export const CSRF_HEADER = 'X-CSRF-Token';
6
+ export function issueCsrfToken() {
7
+ return randomUUID();
8
+ }
9
+ /** Origem esperada: mesma do Host da requisição (ou configurada em TRUSTED_ORIGINS). */
10
+ export function isSameOrigin(c) {
11
+ const origin = c.req.header('Origin') ?? c.req.header('Referer');
12
+ if (!origin)
13
+ return false;
14
+ let originHost;
15
+ try {
16
+ originHost = new URL(origin).host;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ const trusted = (process.env['TRUSTED_ORIGINS'] ?? '')
22
+ .split(',')
23
+ .map((s) => s.trim())
24
+ .filter(Boolean);
25
+ if (trusted.includes(originHost))
26
+ return true;
27
+ const host = c.req.header('X-Forwarded-Host') ?? c.req.header('Host');
28
+ return Boolean(host) && originHost === host;
29
+ }
30
+ function readCookie(cookieHeader, name) {
31
+ if (!cookieHeader)
32
+ return null;
33
+ for (const part of cookieHeader.split(';')) {
34
+ const [key, ...rest] = part.trim().split('=');
35
+ if (key === name)
36
+ return decodeURIComponent(rest.join('='));
37
+ }
38
+ return null;
39
+ }
40
+ /**
41
+ * Double-submit cookie: emite cookie bl_csrf para navegadores e valida que
42
+ * header X-CSRF-Token === cookie em requisições state-changing com cookie de sessão.
43
+ * Clientes não-navegador (Authorization Bearer) ficam isentos.
44
+ */
45
+ export async function csrfMiddleware(c, next) {
46
+ const method = c.req.method.toUpperCase();
47
+ if (!STATE_CHANGING.has(method)) {
48
+ await next();
49
+ return;
50
+ }
51
+ // Autenticação por token (API-first) não é vulnerável a CSRF
52
+ const hasBearer = Boolean(c.req.header('Authorization')?.startsWith('Bearer '));
53
+ const cookieHeader = c.req.header('Cookie');
54
+ if (!hasBearer && cookieHeader?.includes(CSRF_COOKIE)) {
55
+ const cookieToken = readCookie(cookieHeader, CSRF_COOKIE);
56
+ const headerToken = c.req.header(CSRF_HEADER);
57
+ if (!cookieToken || !headerToken || cookieToken !== headerToken || !isSameOrigin(c)) {
58
+ return c.json({ error: 'CSRF_TOKEN_INVALID', message: 'CSRF validation failed.' }, 403);
59
+ }
60
+ }
61
+ await next();
62
+ return;
63
+ }
64
+ //# sourceMappingURL=csrf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf.js","sourceRoot":"","sources":["../../src/middleware/csrf.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=csrf.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf.test.d.ts","sourceRoot":"","sources":["../../src/middleware/csrf.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,94 @@
1
+ // @oktis-works/api - CSRF double-submit (security-001)
2
+ import { describe, it, expect, vi } from 'vitest';
3
+ vi.mock('@oktis-works/core', () => ({
4
+ establishTenantContext: vi.fn(async () => undefined),
5
+ }));
6
+ import { Hono } from 'hono';
7
+ import { csrfMiddleware, issueCsrfToken, CSRF_COOKIE, CSRF_HEADER, isSameOrigin } from './csrf.js';
8
+ function buildApp() {
9
+ const app = new Hono();
10
+ app.use('*', csrfMiddleware);
11
+ app.post('/api/v1/things', (c) => c.json({ ok: true }));
12
+ app.get('/api/v1/things', (c) => c.json({ ok: true }));
13
+ return app;
14
+ }
15
+ describe('csrfMiddleware', () => {
16
+ it('GET passa sem validação', async () => {
17
+ const res = await buildApp().request('/api/v1/things');
18
+ expect(res.status).toBe(200);
19
+ });
20
+ it('POST com Bearer token fica isento (API-first)', async () => {
21
+ const res = await buildApp().request('/api/v1/things', {
22
+ method: 'POST',
23
+ headers: { Authorization: 'Bearer abc' },
24
+ });
25
+ expect(res.status).toBe(200);
26
+ });
27
+ it('POST com cookie de sessão sem header CSRF é bloqueado', async () => {
28
+ const res = await buildApp().request('/api/v1/things', {
29
+ method: 'POST',
30
+ headers: { Cookie: `session=abc; ${CSRF_COOKIE}=tok-1` },
31
+ });
32
+ expect(res.status).toBe(403);
33
+ const body = (await res.json());
34
+ expect(body.error).toBe('CSRF_TOKEN_INVALID');
35
+ });
36
+ it('POST com cookie + header iguais e mesma origem passa', async () => {
37
+ const res = await buildApp().request('/api/v1/things', {
38
+ method: 'POST',
39
+ headers: {
40
+ Host: 'cms.local',
41
+ Origin: 'https://cms.local',
42
+ Cookie: `session=abc; ${CSRF_COOKIE}=tok-1`,
43
+ [CSRF_HEADER]: 'tok-1',
44
+ },
45
+ });
46
+ expect(res.status).toBe(200);
47
+ });
48
+ it('token divergente ou origem cruzada é rejeitado', async () => {
49
+ const app = buildApp();
50
+ const mismatch = await app.request('/api/v1/things', {
51
+ method: 'POST',
52
+ headers: {
53
+ Host: 'cms.local',
54
+ Origin: 'https://cms.local',
55
+ Cookie: `${CSRF_COOKIE}=tok-1`,
56
+ [CSRF_HEADER]: 'tok-other',
57
+ },
58
+ });
59
+ expect(mismatch.status).toBe(403);
60
+ const crossOrigin = await app.request('/api/v1/things', {
61
+ method: 'POST',
62
+ headers: {
63
+ Host: 'cms.local',
64
+ Origin: 'https://evil.example',
65
+ Cookie: `${CSRF_COOKIE}=tok-1`,
66
+ [CSRF_HEADER]: 'tok-1',
67
+ },
68
+ });
69
+ expect(crossOrigin.status).toBe(403);
70
+ });
71
+ it('issueCsrfToken gera tokens únicos', () => {
72
+ expect(issueCsrfToken()).not.toBe(issueCsrfToken());
73
+ });
74
+ });
75
+ describe('isSameOrigin', () => {
76
+ it('aceita host igual e TRUSTED_ORIGINS', () => {
77
+ const ctx = (origin, host = 'cms.local') => ({
78
+ req: {
79
+ header: (name) => name === 'Origin' ? origin : name === 'Host' ? host : undefined,
80
+ },
81
+ });
82
+ process.env['TRUSTED_ORIGINS'] = 'app.okcms.dev';
83
+ try {
84
+ expect(isSameOrigin(ctx('https://cms.local'))).toBe(true);
85
+ expect(isSameOrigin(ctx('https://app.okcms.dev'))).toBe(true);
86
+ expect(isSameOrigin(ctx(undefined))).toBe(false);
87
+ expect(isSameOrigin(ctx('https://evil.example'))).toBe(false);
88
+ }
89
+ finally {
90
+ delete process.env['TRUSTED_ORIGINS'];
91
+ }
92
+ });
93
+ });
94
+ //# sourceMappingURL=csrf.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf.test.js","sourceRoot":"","sources":["../../src/middleware/csrf.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { Context, Next } from 'hono';
2
+ export interface RateLimitOptions {
3
+ max?: number;
4
+ windowMs?: number;
5
+ }
6
+ export declare function parseRateLimitEnv(env?: Record<string, string | undefined>): {
7
+ max: number;
8
+ windowMs: number;
9
+ };
10
+ export declare function clientKey(c: Context): string;
11
+ /** Reseta os buckets (uso em testes). */
12
+ export declare function resetRateLimiter(): void;
13
+ /**
14
+ * Janela deslizante simples: 429 + Retry-After quando excede o limite.
15
+ * /health e /metrics ficam de fora (probes não devem ser limitados).
16
+ */
17
+ export declare function rateLimitMiddleware(options?: RateLimitOptions): (c: Context, next: Next) => Promise<Response | undefined>;
18
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAS1C,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GAAG;IACxF,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB,CAOA;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAO5C;AAED,yCAAyC;AACzC,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,gBAAqB,OAG/C,OAAO,QAAQ,IAAI,KAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAqCrE"}
@@ -0,0 +1,58 @@
1
+ // @oktis-works/api - Rate limiting por janela deslizante in-memory (REQU-020/rest-api-003)
2
+ const buckets = new Map();
3
+ export function parseRateLimitEnv(env = process.env) {
4
+ const max = Number(env['RATE_LIMIT_MAX'] ?? 300);
5
+ const windowSeconds = Number(env['RATE_LIMIT_WINDOW_SECONDS'] ?? 60);
6
+ return {
7
+ max: Number.isFinite(max) && max > 0 ? Math.floor(max) : 300,
8
+ windowMs: Number.isFinite(windowSeconds) && windowSeconds > 0 ? windowSeconds * 1000 : 60_000,
9
+ };
10
+ }
11
+ export function clientKey(c) {
12
+ const tenant = c.req.header('X-Tenant-ID') ?? 'platform';
13
+ const ip = c.req.header('X-Forwarded-For')?.split(',')[0]?.trim() ??
14
+ c.req.header('X-Real-IP') ??
15
+ 'unknown';
16
+ return `${tenant}:${ip}`;
17
+ }
18
+ /** Reseta os buckets (uso em testes). */
19
+ export function resetRateLimiter() {
20
+ buckets.clear();
21
+ }
22
+ /**
23
+ * Janela deslizante simples: 429 + Retry-After quando excede o limite.
24
+ * /health e /metrics ficam de fora (probes não devem ser limitados).
25
+ */
26
+ export function rateLimitMiddleware(options = {}) {
27
+ const { max, windowMs } = { ...parseRateLimitEnv(), ...options };
28
+ return async (c, next) => {
29
+ const path = new URL(c.req.url).pathname;
30
+ if (path === '/health' || path.startsWith('/api/v1/health') || path.startsWith('/metrics')) {
31
+ await next();
32
+ return;
33
+ }
34
+ const key = clientKey(c);
35
+ const now = Date.now();
36
+ const bucket = buckets.get(key);
37
+ if (!bucket || bucket.resetAt <= now) {
38
+ buckets.set(key, { count: 1, resetAt: now + windowMs });
39
+ c.header('X-RateLimit-Limit', String(max));
40
+ c.header('X-RateLimit-Remaining', String(max - 1));
41
+ await next();
42
+ return;
43
+ }
44
+ bucket.count += 1;
45
+ if (bucket.count > max) {
46
+ const retryAfter = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000));
47
+ c.header('Retry-After', String(retryAfter));
48
+ c.header('X-RateLimit-Limit', String(max));
49
+ c.header('X-RateLimit-Remaining', '0');
50
+ return c.json({ error: 'TOO_MANY_REQUESTS', message: `Rate limit exceeded. Retry after ${retryAfter}s.` }, 429);
51
+ }
52
+ c.header('X-RateLimit-Limit', String(max));
53
+ c.header('X-RateLimit-Remaining', String(Math.max(0, max - bucket.count)));
54
+ await next();
55
+ return;
56
+ };
57
+ }
58
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rate-limit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.test.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,58 @@
1
+ // @oktis-works/api - Rate limit (rest-api-003)
2
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
3
+ vi.mock('@oktis-works/core', () => ({
4
+ establishTenantContext: vi.fn(async () => undefined),
5
+ }));
6
+ import { Hono } from 'hono';
7
+ import { rateLimitMiddleware, resetRateLimiter, parseRateLimitEnv } from './rate-limit.js';
8
+ function buildApp(max) {
9
+ const app = new Hono();
10
+ app.use('*', rateLimitMiddleware({ max, windowMs: 60_000 }));
11
+ app.get('/api/v1/things', (c) => c.json({ ok: true }));
12
+ app.get('/health', (c) => c.json({ status: 'ok' }));
13
+ return app;
14
+ }
15
+ beforeEach(() => {
16
+ resetRateLimiter();
17
+ });
18
+ describe('parseRateLimitEnv', () => {
19
+ it('lê env com defaults sensatos', () => {
20
+ expect(parseRateLimitEnv({})).toEqual({ max: 300, windowMs: 60_000 });
21
+ expect(parseRateLimitEnv({ RATE_LIMIT_MAX: '10', RATE_LIMIT_WINDOW_SECONDS: '30' })).toEqual({
22
+ max: 10,
23
+ windowMs: 30_000,
24
+ });
25
+ expect(parseRateLimitEnv({ RATE_LIMIT_MAX: 'abc' })).toEqual({ max: 300, windowMs: 60_000 });
26
+ });
27
+ });
28
+ describe('rateLimitMiddleware', () => {
29
+ it('permite até max requisições e devolve 429 + Retry-After na excedente', async () => {
30
+ const app = buildApp(3);
31
+ const headers = { 'X-Tenant-ID': 't1', 'X-Forwarded-For': '10.0.0.1' };
32
+ for (let i = 0; i < 3; i++) {
33
+ const res = await app.request('/api/v1/things', { headers });
34
+ expect(res.status).toBe(200);
35
+ }
36
+ const blocked = await app.request('/api/v1/things', { headers });
37
+ expect(blocked.status).toBe(429);
38
+ expect(Number(blocked.headers.get('Retry-After'))).toBeGreaterThan(0);
39
+ expect(blocked.headers.get('X-RateLimit-Limit')).toBe('3');
40
+ const body = (await blocked.json());
41
+ expect(body.error).toBe('TOO_MANY_REQUESTS');
42
+ });
43
+ it('buckets separados por tenant+IP', async () => {
44
+ const app = buildApp(1);
45
+ const h1 = { 'X-Tenant-ID': 't1', 'X-Forwarded-For': '10.0.0.1' };
46
+ const h2 = { 'X-Tenant-ID': 't2', 'X-Forwarded-For': '10.0.0.2' };
47
+ expect((await app.request('/api/v1/things', { headers: h1 })).status).toBe(200);
48
+ expect((await app.request('/api/v1/things', { headers: h1 })).status).toBe(429);
49
+ expect((await app.request('/api/v1/things', { headers: h2 })).status).toBe(200);
50
+ });
51
+ it('health fica isento do limite', async () => {
52
+ const app = buildApp(1);
53
+ expect((await app.request('/health')).status).toBe(200);
54
+ expect((await app.request('/health')).status).toBe(200);
55
+ expect((await app.request('/health')).status).toBe(200);
56
+ });
57
+ });
58
+ //# sourceMappingURL=rate-limit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.test.js","sourceRoot":"","sources":["../../src/middleware/rate-limit.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { Context, Next } from 'hono';
2
+ /**
3
+ * REQ-observability-005: todo request pode ser rastreado via trace ID.
4
+ * Propaga X-Trace-ID recebido ou gera um novo; devolve na resposta.
5
+ */
6
+ export declare function traceMiddleware(c: Context, next: Next): Promise<Response | void>;
7
+ //# sourceMappingURL=trace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../src/middleware/trace.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG1C;;;GAGG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAOtF"}