@etus/bhono-app 0.1.6 → 0.1.7

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 (61) hide show
  1. package/package.json +3 -2
  2. package/templates/base/.claude/commands/check-skill-rules.md +112 -29
  3. package/templates/base/.claude/commands/linear/implement-issue.md +383 -55
  4. package/templates/base/.claude/commands/ship.md +77 -13
  5. package/templates/base/.claude/hooks/package-lock.json +0 -419
  6. package/templates/base/.claude/hooks/skill-activation-prompt.ts +185 -113
  7. package/templates/base/.claude/hooks/skill-tool-guard.sh +6 -0
  8. package/templates/base/.claude/hooks/skill-tool-guard.ts +198 -0
  9. package/templates/base/.claude/scripts/validate-skill-rules.sh +55 -32
  10. package/templates/base/.claude/settings.json +18 -11
  11. package/templates/base/.claude/skills/skill-rules.json +326 -173
  12. package/templates/base/.env.example +3 -0
  13. package/templates/base/README.md +9 -7
  14. package/templates/base/config/eslint.config.js +1 -0
  15. package/templates/base/config/wrangler.json +16 -17
  16. package/templates/base/docs/SETUP-GUIDE.md +566 -0
  17. package/templates/base/docs/architecture/README.md +162 -8
  18. package/templates/base/docs/architecture/api-catalog.md +575 -0
  19. package/templates/base/docs/architecture/c4-component.md +309 -0
  20. package/templates/base/docs/architecture/c4-container.md +183 -0
  21. package/templates/base/docs/architecture/c4-context.md +106 -0
  22. package/templates/base/docs/architecture/dependencies.md +327 -0
  23. package/templates/base/docs/architecture/tech-debt.md +184 -0
  24. package/templates/base/package.json +20 -15
  25. package/templates/base/scripts/capture-prod-session.ts +2 -2
  26. package/templates/base/scripts/sync-template.sh +104 -0
  27. package/templates/base/src/server/db/sql.ts +24 -4
  28. package/templates/base/src/server/index.ts +1 -0
  29. package/templates/base/src/server/lib/audited-db.ts +10 -10
  30. package/templates/base/src/server/middleware/account.ts +1 -1
  31. package/templates/base/src/server/middleware/auth.ts +11 -11
  32. package/templates/base/src/server/middleware/rate-limit.ts +3 -6
  33. package/templates/base/src/server/routes/auth/handlers.ts +5 -5
  34. package/templates/base/src/server/routes/auth/test-login.ts +9 -9
  35. package/templates/base/src/server/routes/index.ts +9 -0
  36. package/templates/base/src/server/routes/invitations/handlers.ts +6 -6
  37. package/templates/base/src/server/routes/openapi.ts +1 -1
  38. package/templates/base/src/server/services/accounts.ts +9 -9
  39. package/templates/base/src/server/services/audits.ts +12 -12
  40. package/templates/base/src/server/services/auth.ts +15 -15
  41. package/templates/base/src/server/services/invitations.ts +16 -16
  42. package/templates/base/src/server/services/users.ts +13 -13
  43. package/templates/base/src/shared/types/api.ts +66 -198
  44. package/templates/base/tests/e2e/auth.setup.ts +1 -1
  45. package/templates/base/tests/unit/server/auth/guards.test.ts +1 -1
  46. package/templates/base/tests/unit/server/middleware/auth.test.ts +273 -0
  47. package/templates/base/tests/unit/server/routes/auth/handlers.test.ts +111 -0
  48. package/templates/base/tests/unit/server/routes/users/handlers.test.ts +69 -5
  49. package/templates/base/tests/unit/server/services/accounts.test.ts +148 -0
  50. package/templates/base/tests/unit/server/services/audits.test.ts +219 -0
  51. package/templates/base/tests/unit/server/services/auth.test.ts +480 -3
  52. package/templates/base/tests/unit/server/services/invitations.test.ts +178 -0
  53. package/templates/base/tests/unit/server/services/users.test.ts +363 -8
  54. package/templates/base/tests/unit/shared/schemas.test.ts +1 -1
  55. package/templates/base/vite.config.ts +3 -1
  56. package/templates/base/.github/workflows/test.yml +0 -127
  57. package/templates/base/.husky/pre-push +0 -26
  58. package/templates/base/auth-setup-error.png +0 -0
  59. package/templates/base/pnpm-lock.yaml +0 -8052
  60. package/templates/base/tests/e2e/_auth/.gitkeep +0 -0
  61. package/templates/base/tsconfig.tsbuildinfo +0 -1
@@ -7,6 +7,17 @@ import { createUserFixture, createSuperAdminFixture } from '@tests/fixtures/serv
7
7
  vi.mock('@server/db/sql', () => ({
8
8
  queryOne: vi.fn(),
9
9
  queryAll: vi.fn(),
10
+ toStringValue: (value: unknown) => {
11
+ if (typeof value === 'string') return value
12
+ if (typeof value === 'number' || typeof value === 'bigint') return String(value)
13
+ return ''
14
+ },
15
+ toNullableString: (value: unknown) => {
16
+ if (value === null || value === undefined) return null
17
+ if (typeof value === 'string') return value
18
+ if (typeof value === 'number' || typeof value === 'bigint') return String(value)
19
+ return null
20
+ },
10
21
  }))
11
22
 
12
23
  import { queryOne, queryAll } from '@server/db/sql'
@@ -137,5 +148,213 @@ describe('auditsService', () => {
137
148
 
138
149
  expect(result.data).toHaveLength(1)
139
150
  })
151
+
152
+ it('should filter by entityId when provided', async () => {
153
+ ;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
154
+ ;(queryAll as Mock).mockResolvedValueOnce([{
155
+ id: 'log-1',
156
+ transactionId: 'tx-1',
157
+ accountId: 'account-123',
158
+ userId: 'user-123',
159
+ entity: 'user',
160
+ entityId: 'specific-entity-id',
161
+ action: 'INSERT',
162
+ changes: null,
163
+ ipAddress: '127.0.0.1',
164
+ userAgent: 'TestAgent',
165
+ timestamp: '2025-01-01T00:00:00Z',
166
+ }])
167
+
168
+ const result = await auditsService.findAll(db, superAdminCtx, { ...defaultFilters, entityId: 'specific-entity-id' })
169
+
170
+ expect(result.data).toHaveLength(1)
171
+ expect(result.data[0].entityId).toBe('specific-entity-id')
172
+ })
173
+
174
+ it('should filter by action when provided', async () => {
175
+ ;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
176
+ ;(queryAll as Mock).mockResolvedValueOnce([{
177
+ id: 'log-1',
178
+ transactionId: 'tx-1',
179
+ accountId: 'account-123',
180
+ userId: 'user-123',
181
+ entity: 'user',
182
+ entityId: 'user-456',
183
+ action: 'DELETE',
184
+ changes: null,
185
+ ipAddress: '127.0.0.1',
186
+ userAgent: 'TestAgent',
187
+ timestamp: '2025-01-01T00:00:00Z',
188
+ }])
189
+
190
+ const result = await auditsService.findAll(db, superAdminCtx, { ...defaultFilters, action: 'DELETE' })
191
+
192
+ expect(result.data).toHaveLength(1)
193
+ expect(result.data[0].action).toBe('DELETE')
194
+ })
195
+
196
+ it('should handle null count result', async () => {
197
+ ;(queryOne as Mock).mockResolvedValueOnce(null)
198
+ ;(queryAll as Mock).mockResolvedValueOnce([])
199
+
200
+ const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
201
+
202
+ expect(result.data).toHaveLength(0)
203
+ expect(result.meta.totalItems).toBe(0)
204
+ })
205
+
206
+ it('should handle snake_case column names', async () => {
207
+ ;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
208
+ ;(queryAll as Mock).mockResolvedValueOnce([{
209
+ id: 'log-1',
210
+ transaction_id: 'tx-snake',
211
+ account_id: 'account-snake',
212
+ user_id: 'user-snake',
213
+ entity: 'user',
214
+ entity_id: 'entity-snake',
215
+ action: 'UPDATE',
216
+ changes: '{"field": "value"}',
217
+ ip_address: '192.168.1.1',
218
+ user_agent: 'SnakeAgent',
219
+ timestamp: '2025-01-01T00:00:00Z',
220
+ }])
221
+
222
+ const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
223
+
224
+ expect(result.data[0].transactionId).toBe('tx-snake')
225
+ expect(result.data[0].accountId).toBe('account-snake')
226
+ expect(result.data[0].userId).toBe('user-snake')
227
+ expect(result.data[0].entityId).toBe('entity-snake')
228
+ expect(result.data[0].ipAddress).toBe('192.168.1.1')
229
+ expect(result.data[0].userAgent).toBe('SnakeAgent')
230
+ })
231
+
232
+ it('should parse changes as JSON string', async () => {
233
+ ;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
234
+ ;(queryAll as Mock).mockResolvedValueOnce([{
235
+ id: 'log-1',
236
+ transactionId: 'tx-1',
237
+ accountId: 'account-123',
238
+ userId: 'user-123',
239
+ entity: 'user',
240
+ entityId: 'user-456',
241
+ action: 'UPDATE',
242
+ changes: '{"old": "value", "new": "changed"}',
243
+ ipAddress: null,
244
+ userAgent: null,
245
+ timestamp: '2025-01-01T00:00:00Z',
246
+ }])
247
+
248
+ const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
249
+
250
+ expect(result.data[0].changes).toEqual({ old: 'value', new: 'changed' })
251
+ })
252
+
253
+ it('should parse changes as object', async () => {
254
+ ;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
255
+ ;(queryAll as Mock).mockResolvedValueOnce([{
256
+ id: 'log-1',
257
+ transactionId: 'tx-1',
258
+ accountId: 'account-123',
259
+ userId: 'user-123',
260
+ entity: 'user',
261
+ entityId: 'user-456',
262
+ action: 'UPDATE',
263
+ changes: { already: 'object' },
264
+ ipAddress: null,
265
+ userAgent: null,
266
+ timestamp: '2025-01-01T00:00:00Z',
267
+ }])
268
+
269
+ const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
270
+
271
+ expect(result.data[0].changes).toEqual({ already: 'object' })
272
+ })
273
+
274
+ it('should handle invalid JSON in changes', async () => {
275
+ ;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
276
+ ;(queryAll as Mock).mockResolvedValueOnce([{
277
+ id: 'log-1',
278
+ transactionId: 'tx-1',
279
+ accountId: 'account-123',
280
+ userId: 'user-123',
281
+ entity: 'user',
282
+ entityId: 'user-456',
283
+ action: 'UPDATE',
284
+ changes: 'not-valid-json',
285
+ ipAddress: null,
286
+ userAgent: null,
287
+ timestamp: '2025-01-01T00:00:00Z',
288
+ }])
289
+
290
+ const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
291
+
292
+ expect(result.data[0].changes).toBeNull()
293
+ })
294
+
295
+ it('should handle null/undefined changes', async () => {
296
+ ;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
297
+ ;(queryAll as Mock).mockResolvedValueOnce([{
298
+ id: 'log-1',
299
+ transactionId: 'tx-1',
300
+ accountId: 'account-123',
301
+ userId: 'user-123',
302
+ entity: 'user',
303
+ entityId: 'user-456',
304
+ action: 'DELETE',
305
+ changes: null,
306
+ ipAddress: null,
307
+ userAgent: null,
308
+ timestamp: '2025-01-01T00:00:00Z',
309
+ }])
310
+
311
+ const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
312
+
313
+ expect(result.data[0].changes).toBeNull()
314
+ })
315
+
316
+ it('should handle null accountId and userId', async () => {
317
+ ;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
318
+ ;(queryAll as Mock).mockResolvedValueOnce([{
319
+ id: 'log-1',
320
+ transactionId: 'tx-1',
321
+ accountId: null,
322
+ userId: null,
323
+ entity: 'system',
324
+ entityId: 'sys-1',
325
+ action: 'INSERT',
326
+ changes: null,
327
+ ipAddress: null,
328
+ userAgent: null,
329
+ timestamp: '2025-01-01T00:00:00Z',
330
+ }])
331
+
332
+ const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
333
+
334
+ expect(result.data[0].accountId).toBeNull()
335
+ expect(result.data[0].userId).toBeNull()
336
+ })
337
+
338
+ it('should handle non-object parsed JSON in changes', async () => {
339
+ ;(queryOne as Mock).mockResolvedValueOnce({ count: 1 })
340
+ ;(queryAll as Mock).mockResolvedValueOnce([{
341
+ id: 'log-1',
342
+ transactionId: 'tx-1',
343
+ accountId: 'account-123',
344
+ userId: 'user-123',
345
+ entity: 'user',
346
+ entityId: 'user-456',
347
+ action: 'UPDATE',
348
+ changes: '"just a string"',
349
+ ipAddress: null,
350
+ userAgent: null,
351
+ timestamp: '2025-01-01T00:00:00Z',
352
+ }])
353
+
354
+ const result = await auditsService.findAll(db, superAdminCtx, defaultFilters)
355
+
356
+ // A JSON string that parses to a primitive should return null
357
+ expect(result.data[0].changes).toBeNull()
358
+ })
140
359
  })
141
360
  })