@kyro-cms/core 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +3 -3
  2. package/dist/api-handler.cjs +6 -6
  3. package/dist/api-handler.js +5 -5
  4. package/dist/{chunk-X3CU27OO.cjs → chunk-3FW6WVVP.cjs} +2 -17
  5. package/dist/chunk-3FW6WVVP.cjs.map +1 -0
  6. package/dist/{chunk-CZ3HWX2X.cjs → chunk-3ZZPZYCM.cjs} +42 -67
  7. package/dist/chunk-3ZZPZYCM.cjs.map +1 -0
  8. package/dist/{chunk-6LPNEC6D.js → chunk-C4JJEE42.js} +43 -68
  9. package/dist/chunk-C4JJEE42.js.map +1 -0
  10. package/dist/{chunk-VEI5KQVC.cjs → chunk-FWGHXRRI.cjs} +45 -15
  11. package/dist/chunk-FWGHXRRI.cjs.map +1 -0
  12. package/dist/{chunk-MMYAIYHJ.cjs → chunk-M4GFA2UQ.cjs} +336 -32
  13. package/dist/chunk-M4GFA2UQ.cjs.map +1 -0
  14. package/dist/{chunk-2SJATAN4.js → chunk-OJBK3JYF.js} +336 -32
  15. package/dist/chunk-OJBK3JYF.js.map +1 -0
  16. package/dist/{chunk-XIXGJGQW.js → chunk-SAMZQVC2.js} +44 -14
  17. package/dist/chunk-SAMZQVC2.js.map +1 -0
  18. package/dist/{chunk-B76I67F3.js → chunk-WSCJQI2B.js} +61 -13
  19. package/dist/chunk-WSCJQI2B.js.map +1 -0
  20. package/dist/{chunk-RGIQKTZ7.js → chunk-YMG55RSX.js} +4 -18
  21. package/dist/chunk-YMG55RSX.js.map +1 -0
  22. package/dist/{chunk-DAIBBBOL.cjs → chunk-Z2OVHWHB.cjs} +61 -13
  23. package/dist/chunk-Z2OVHWHB.cjs.map +1 -0
  24. package/dist/drizzle/index.cjs +10 -10
  25. package/dist/drizzle/index.js +2 -2
  26. package/dist/index.cjs +138 -105
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.js +77 -44
  29. package/dist/index.js.map +1 -1
  30. package/dist/rest/index.cjs +5 -4
  31. package/dist/rest/index.js +3 -2
  32. package/dist/templates/index.cjs +24 -24
  33. package/dist/templates/index.js +1 -1
  34. package/package.json +2 -14
  35. package/dist/WebhookService-BCpW2dyL.d.ts +0 -112
  36. package/dist/WebhookService-DxYSFvNg.d.cts +0 -112
  37. package/dist/api-handler.d.cts +0 -9
  38. package/dist/api-handler.d.ts +0 -9
  39. package/dist/base-DvvNqnM-.d.cts +0 -73
  40. package/dist/base-eVegJ_Pr.d.ts +0 -73
  41. package/dist/chunk-2SJATAN4.js.map +0 -1
  42. package/dist/chunk-6LPNEC6D.js.map +0 -1
  43. package/dist/chunk-B76I67F3.js.map +0 -1
  44. package/dist/chunk-CZ3HWX2X.cjs.map +0 -1
  45. package/dist/chunk-DAIBBBOL.cjs.map +0 -1
  46. package/dist/chunk-MMYAIYHJ.cjs.map +0 -1
  47. package/dist/chunk-RGIQKTZ7.js.map +0 -1
  48. package/dist/chunk-VEI5KQVC.cjs.map +0 -1
  49. package/dist/chunk-X3CU27OO.cjs.map +0 -1
  50. package/dist/chunk-XIXGJGQW.js.map +0 -1
  51. package/dist/cli/index.d.cts +0 -1
  52. package/dist/cli/index.d.ts +0 -1
  53. package/dist/client.d.cts +0 -12
  54. package/dist/client.d.ts +0 -12
  55. package/dist/drizzle/index.d.cts +0 -135
  56. package/dist/drizzle/index.d.ts +0 -135
  57. package/dist/fields/index.d.cts +0 -27
  58. package/dist/fields/index.d.ts +0 -27
  59. package/dist/graphql/index.d.cts +0 -22
  60. package/dist/graphql/index.d.ts +0 -22
  61. package/dist/index-Bz9JqRGI.d.cts +0 -86
  62. package/dist/index-Bz9JqRGI.d.ts +0 -86
  63. package/dist/index-CLp-DRKA.d.ts +0 -64
  64. package/dist/index-DfO7G4kN.d.cts +0 -64
  65. package/dist/index.d.cts +0 -1361
  66. package/dist/index.d.ts +0 -1361
  67. package/dist/integration.d.cts +0 -27
  68. package/dist/integration.d.ts +0 -27
  69. package/dist/mongodb/index.d.cts +0 -63
  70. package/dist/mongodb/index.d.ts +0 -63
  71. package/dist/mysql-media-AI6YK767.cjs +0 -48
  72. package/dist/mysql-media-AI6YK767.cjs.map +0 -1
  73. package/dist/mysql-media-CDZUS7YX.js +0 -45
  74. package/dist/mysql-media-CDZUS7YX.js.map +0 -1
  75. package/dist/rest/index.d.cts +0 -57
  76. package/dist/rest/index.d.ts +0 -57
  77. package/dist/templates/index.d.cts +0 -59
  78. package/dist/templates/index.d.ts +0 -59
  79. package/dist/trpc/index.d.cts +0 -136
  80. package/dist/trpc/index.d.ts +0 -136
  81. package/dist/types-Bs1up4yP.d.ts +0 -461
  82. package/dist/types-Da83JLDk.d.cts +0 -130
  83. package/dist/types-Da83JLDk.d.ts +0 -130
  84. package/dist/types-J3R9nVsZ.d.cts +0 -461
  85. package/dist/types-VtjUxIMp.d.cts +0 -246
  86. package/dist/types-VtjUxIMp.d.ts +0 -246
  87. package/dist/ws/index.d.cts +0 -88
  88. package/dist/ws/index.d.ts +0 -88
@@ -1,8 +1,9 @@
1
1
  import { PasswordPolicy, ConfigService, SQLiteAuthAdapter, EmailTransport } from './chunk-RYDGMBIG.js';
2
2
  import { createAuditContext } from './chunk-P2YW545G.js';
3
- import { WEBHOOK_EVENTS, API_KEY_COLLECTION, generateApiKey, generateApiKeyPrefix, hasApiKeyPermission, extractApiKeyFromRequest, validateApiKey, createApiKeyContext } from './chunk-QXIQWPAP.js';
3
+ import { WEBHOOK_EVENTS, extractApiKeyFromRequest, validateApiKey, createApiKeyContext, API_KEY_COLLECTION, generateApiKey, generateApiKeyPrefix, hasApiKeyPermission } from './chunk-QXIQWPAP.js';
4
+ import { buildGraphQLSchema } from './chunk-REK7AYOC.js';
4
5
  import { evaluateAccess } from './chunk-SDMNUYVU.js';
5
- import { genId } from './chunk-RGIQKTZ7.js';
6
+ import { genId } from './chunk-YMG55RSX.js';
6
7
  import { __esm, __export, __toCommonJS, __require } from './chunk-Z6ZWNWWR.js';
7
8
  import crypto, { randomBytes, createHash } from 'crypto';
8
9
  import { createStorage } from 'unstorage';
@@ -11,6 +12,7 @@ import { Hono } from 'hono';
11
12
  import path, { join, basename, extname, resolve } from 'path';
12
13
  import { existsSync, readFileSync } from 'fs';
13
14
  import sharp from 'sharp';
15
+ import { parse, execute } from 'graphql';
14
16
  import { mkdir, readdir, stat, rename, unlink, writeFile } from 'fs/promises';
15
17
  import { S3Client, ListObjectsV2Command, DeleteObjectsCommand, DeleteObjectCommand, PutObjectCommand, HeadObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
16
18
  import { Readable } from 'stream';
@@ -1351,7 +1353,7 @@ var AuthRoutes = class {
1351
1353
  }
1352
1354
  try {
1353
1355
  const sessions = await getUserSessions(session.userId, getSessionIdFromRequest(req) ?? void 0);
1354
- return this.jsonResponse({ success: true, sessions });
1356
+ return this.jsonResponse({ sessions });
1355
1357
  } catch (error) {
1356
1358
  console.error("[AuthRoutes.listSessions] Error:", error);
1357
1359
  return this.errorResponse("Failed to list sessions", 500);
@@ -2842,7 +2844,7 @@ var MediaService = class _MediaService {
2842
2844
  ]
2843
2845
  );
2844
2846
  } else {
2845
- const { media: mediaSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
2847
+ const { media: mediaSchema } = await import('./media-HOT3O7RW.js');
2846
2848
  await this.db.insert(mediaSchema).values({
2847
2849
  id,
2848
2850
  filename: storageResult.filename,
@@ -2898,7 +2900,7 @@ var MediaService = class _MediaService {
2898
2900
  id
2899
2901
  ]);
2900
2902
  } else {
2901
- const { media: mediaSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
2903
+ const { media: mediaSchema } = await import('./media-HOT3O7RW.js');
2902
2904
  const [row] = await this.db.select().from(mediaSchema).where(mediaSchema.id.equals(id));
2903
2905
  if (row) item = this.rowToMedia(row);
2904
2906
  }
@@ -2913,7 +2915,7 @@ var MediaService = class _MediaService {
2913
2915
  if (this.dialect === "sqlite") {
2914
2916
  await this.sqliteRun(`DELETE FROM ${this.mediaTable} WHERE id = ?`, [id]);
2915
2917
  } else {
2916
- const { media: mediaSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
2918
+ const { media: mediaSchema } = await import('./media-HOT3O7RW.js');
2917
2919
  await this.db.delete(mediaSchema).where(mediaSchema.id.equals(id));
2918
2920
  }
2919
2921
  }
@@ -2927,7 +2929,7 @@ var MediaService = class _MediaService {
2927
2929
  id
2928
2930
  ]);
2929
2931
  } else {
2930
- const { media: mediaSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
2932
+ const { media: mediaSchema } = await import('./media-HOT3O7RW.js');
2931
2933
  const [row] = await this.db.select().from(mediaSchema).where(mediaSchema.id.equals(id));
2932
2934
  if (row) item = this.rowToMedia(row);
2933
2935
  }
@@ -2961,7 +2963,7 @@ var MediaService = class _MediaService {
2961
2963
  [...vals, this.now(), id]
2962
2964
  );
2963
2965
  } else {
2964
- const { media: mediaSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
2966
+ const { media: mediaSchema } = await import('./media-HOT3O7RW.js');
2965
2967
  await this.db.update(mediaSchema).set({ ...updateData, updatedAt: this.now() }).where(mediaSchema.id.equals(id));
2966
2968
  }
2967
2969
  return {
@@ -2979,7 +2981,7 @@ var MediaService = class _MediaService {
2979
2981
  );
2980
2982
  return row2 ? this.rowToMedia(row2) : null;
2981
2983
  }
2982
- const { media: mediaSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
2984
+ const { media: mediaSchema } = await import('./media-HOT3O7RW.js');
2983
2985
  const [row] = await this.db.select().from(mediaSchema).where(mediaSchema.id.equals(id));
2984
2986
  return row ? this.rowToMedia(row) : null;
2985
2987
  }
@@ -3026,8 +3028,8 @@ var MediaService = class _MediaService {
3026
3028
  totalPages: Math.ceil(totalDocs2 / limit)
3027
3029
  };
3028
3030
  }
3029
- const { media: mediaSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
3030
- const { like, or, and, asc, desc, eq, sql } = await (this.dialect === "mysql" ? import('drizzle-orm/mysql-core') : import('drizzle-orm/pg-core'));
3031
+ const { media: mediaSchema } = await import('./media-HOT3O7RW.js');
3032
+ const { like, or, and, asc, desc, eq, sql } = await import('drizzle-orm/pg-core');
3031
3033
  const conditions = [];
3032
3034
  if (search) {
3033
3035
  conditions.push(
@@ -3095,7 +3097,7 @@ var MediaService = class _MediaService {
3095
3097
  );
3096
3098
  return row ? this.rowToMedia(row) : null;
3097
3099
  }
3098
- const { media: mediaSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
3100
+ const { media: mediaSchema } = await import('./media-HOT3O7RW.js');
3099
3101
  const [updated] = await this.db.update(mediaSchema).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(mediaSchema.id.equals(id)).returning();
3100
3102
  return updated ? this.rowToMedia(updated) : null;
3101
3103
  }
@@ -3116,7 +3118,7 @@ var MediaService = class _MediaService {
3116
3118
  );
3117
3119
  }
3118
3120
  } else {
3119
- const { media: mediaSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
3121
+ const { media: mediaSchema } = await import('./media-HOT3O7RW.js');
3120
3122
  for (const id of ids) {
3121
3123
  await this.db.update(mediaSchema).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(mediaSchema.id.equals(id));
3122
3124
  }
@@ -3130,8 +3132,8 @@ var MediaService = class _MediaService {
3130
3132
  );
3131
3133
  return rows.map((r) => r.path).filter((f) => f && f !== "").sort();
3132
3134
  }
3133
- const { media: mediaSchema, mediaFolders: folderSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
3134
- const { eq, sql } = await (this.dialect === "mysql" ? import('drizzle-orm/mysql-core') : import('drizzle-orm/pg-core'));
3135
+ const { media: mediaSchema, mediaFolders: folderSchema } = await import('./media-HOT3O7RW.js');
3136
+ const { eq, sql } = await import('drizzle-orm/pg-core');
3135
3137
  const fromMedia = await this.db.select({ folder: mediaSchema.folder }).from(mediaSchema).groupBy(mediaSchema.folder);
3136
3138
  const fromFolders = await this.db.select({ path: folderSchema.path }).from(folderSchema);
3137
3139
  const allPaths = /* @__PURE__ */ new Set([
@@ -3150,7 +3152,7 @@ var MediaService = class _MediaService {
3150
3152
  [fullPath, name, parentPath || null, now]
3151
3153
  );
3152
3154
  } else {
3153
- const { mediaFolders: folderSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
3155
+ const { mediaFolders: folderSchema } = await import('./media-HOT3O7RW.js');
3154
3156
  await this.db.insert(folderSchema).values({
3155
3157
  path: fullPath,
3156
3158
  name,
@@ -3171,8 +3173,8 @@ var MediaService = class _MediaService {
3171
3173
  [folder, `${folder}/%`]
3172
3174
  );
3173
3175
  } else {
3174
- const { mediaFolders: folderSchema } = await (this.dialect === "mysql" ? import('./mysql-media-CDZUS7YX.js') : import('./media-HOT3O7RW.js'));
3175
- const { like, or, eq } = await (this.dialect === "mysql" ? import('drizzle-orm/mysql-core') : import('drizzle-orm/pg-core'));
3176
+ const { mediaFolders: folderSchema } = await import('./media-HOT3O7RW.js');
3177
+ const { like, or, eq } = await import('./media-HOT3O7RW.js');
3176
3178
  await this.db.delete(folderSchema).where(
3177
3179
  or(
3178
3180
  eq(folderSchema.path, folder),
@@ -3373,6 +3375,27 @@ function getWebhookEvent(collection, operation) {
3373
3375
  if (mapped) return mapped[operation];
3374
3376
  return `collection.${operation}`;
3375
3377
  }
3378
+ function extractIp(req) {
3379
+ const forwarded = req.headers.get("x-forwarded-for");
3380
+ if (forwarded) return forwarded.split(",")[0].trim();
3381
+ return req.headers.get("x-real-ip") || "unknown";
3382
+ }
3383
+ function auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, endpoint, method, req) {
3384
+ if (apiKeyContext?.apiKeyId && sessionAuthAdapter) {
3385
+ sessionAuthAdapter.createAuditLog({
3386
+ action: "api_request",
3387
+ userId: apiKeyContext.userId || "",
3388
+ resource: "api_key",
3389
+ resourceId: apiKeyContext.apiKeyId,
3390
+ success: true,
3391
+ metadata: {
3392
+ endpoint,
3393
+ method,
3394
+ ip: extractIp(req)
3395
+ }
3396
+ });
3397
+ }
3398
+ }
3376
3399
  function readBaseUpdatedAt(body) {
3377
3400
  return body.baseUpdatedAt ?? body._baseUpdatedAt;
3378
3401
  }
@@ -3515,7 +3538,8 @@ async function resolveAuthContext(req, authMw, staticUser, staticTenantID) {
3515
3538
  return {
3516
3539
  user: result.user || staticUser,
3517
3540
  tenantID: result.tenantContext?.tenantId || staticTenantID,
3518
- apiKeyContext: result.apiKeyContext
3541
+ apiKeyContext: result.apiKeyContext,
3542
+ authType: result.authType
3519
3543
  };
3520
3544
  }
3521
3545
  function createHonoApp(options) {
@@ -3630,6 +3654,69 @@ function createHonoApp(options) {
3630
3654
  app.delete("/api/auth/sessions", async (c) => authRoutes.revokeOtherSessions(c.req.raw));
3631
3655
  app.delete("/api/auth/sessions/:id", async (c) => authRoutes.revokeSession(c.req.raw, c.req.param("id")));
3632
3656
  app.put("/api/auth/sessions/:id/name", async (c) => authRoutes.renameSession(c.req.raw, c.req.param("id")));
3657
+ app.post("/api/graphql", async (c) => {
3658
+ try {
3659
+ const req = c.req.raw;
3660
+ const apiKeyRaw = extractApiKeyFromRequest(req);
3661
+ if (apiKeyRaw && db) {
3662
+ const apiKeyResult = await validateApiKey(apiKeyRaw, db);
3663
+ if (!apiKeyResult.valid) {
3664
+ return c.json({ errors: [{ message: apiKeyResult.error || "Invalid API key" }] }, 401);
3665
+ }
3666
+ const apiKeyId = apiKeyResult.apiKeyId || "";
3667
+ await sessionAuthAdapter?.createAuditLog({
3668
+ action: "api_key_request",
3669
+ userId: apiKeyResult.userId || "",
3670
+ resource: "api_key",
3671
+ resourceId: apiKeyId,
3672
+ success: true,
3673
+ metadata: {
3674
+ endpoint: "/api/graphql",
3675
+ method: "POST",
3676
+ ip: extractIp(req)
3677
+ }
3678
+ });
3679
+ }
3680
+ const body = await req.json().catch(() => ({}));
3681
+ const { query, variables } = body;
3682
+ if (!query) {
3683
+ return c.json({ errors: [{ message: "No query provided" }] }, 400);
3684
+ }
3685
+ let gqlUser;
3686
+ let apiKeyCtx;
3687
+ if (apiKeyRaw && db) {
3688
+ const apiKeyResult = await validateApiKey(apiKeyRaw, db);
3689
+ if (apiKeyResult.valid && apiKeyResult.user) {
3690
+ gqlUser = apiKeyResult.user;
3691
+ apiKeyCtx = createApiKeyContext(apiKeyResult);
3692
+ }
3693
+ }
3694
+ const schema = buildGraphQLSchema({
3695
+ registry,
3696
+ db,
3697
+ user: gqlUser,
3698
+ req,
3699
+ settings
3700
+ });
3701
+ const document = parse(query);
3702
+ const result = await execute({
3703
+ schema,
3704
+ document,
3705
+ variableValues: variables,
3706
+ contextValue: { user: gqlUser, apiKeyContext: apiKeyCtx, req, db }
3707
+ });
3708
+ return c.json(result);
3709
+ } catch (error) {
3710
+ if (error.message?.includes("GraphQL is disabled")) {
3711
+ return c.json({ errors: [{ message: "GraphQL API is disabled" }] }, 403);
3712
+ }
3713
+ if (error instanceof SyntaxError) {
3714
+ return c.json({ errors: [{ message: "Invalid request body" }] }, 400);
3715
+ }
3716
+ console.error("[GraphQL] execution error:", error);
3717
+ return c.json({ errors: [{ message: error.message || "GraphQL execution failed" }] }, 500);
3718
+ }
3719
+ });
3633
3720
  app.get("/api/auth/access", async (c) => {
3634
3721
  try {
3635
3722
  const { user: ctxUser, tenantID: ctxTenantID } = await resolveAuthContext(
@@ -4344,6 +4431,7 @@ function createHonoApp(options) {
4344
4431
  key: rawKey,
4345
4432
  keyPrefix: generateApiKeyPrefix(rawKey),
4346
4433
  permissions: Array.isArray(body.permissions) ? body.permissions : ["*"],
4434
+ expiresAt: body.expiresAt || null,
4347
4435
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
4348
4436
  }
4349
4437
  });
@@ -4389,6 +4477,201 @@ function createHonoApp(options) {
4389
4477
  return c.json({ error: error.message }, 500);
4390
4478
  }
4391
4479
  });
4480
+ app.patch("/api/keys/:id", async (c) => {
4481
+ try {
4482
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4483
+ if (!ctxUser || !hasPermission(ctxUser, "users:admin")) {
4484
+ return c.json({ error: "Forbidden" }, 403);
4485
+ }
4486
+ const id = c.req.param("id");
4487
+ const body = await c.req.json();
4488
+ const existing = await db.findByID({ collection: API_KEY_COLLECTION, id });
4489
+ if (!existing) return c.json({ error: "API key not found" }, 404);
4490
+ const updateData = {};
4491
+ if (typeof body.name === "string" && body.name.trim()) updateData.name = body.name.trim();
4492
+ if (Array.isArray(body.permissions)) updateData.permissions = body.permissions;
4493
+ if (body.expiresAt !== void 0) updateData.expiresAt = body.expiresAt || null;
4494
+ if (Object.keys(updateData).length === 0) return c.json({ error: "Nothing to update" }, 400);
4495
+ const updated = await db.update({ collection: API_KEY_COLLECTION, id, data: updateData });
4496
+ return c.json({ ...updated, keyPrefix: existing.keyPrefix });
4497
+ } catch (error) {
4498
+ console.error("[ApiKeys] PATCH error:", error);
4499
+ return c.json({ error: error.message }, 500);
4500
+ }
4501
+ });
4502
+ app.post("/api/keys/:id/rotate", async (c) => {
4503
+ try {
4504
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4505
+ if (!ctxUser || !hasPermission(ctxUser, "users:admin")) {
4506
+ return c.json({ error: "Forbidden" }, 403);
4507
+ }
4508
+ const id = c.req.param("id");
4509
+ const existing = await db.findByID({ collection: API_KEY_COLLECTION, id });
4510
+ if (!existing) return c.json({ error: "API key not found" }, 404);
4511
+ const rawKey = generateApiKey();
4512
+ const updated = await db.update({
4513
+ collection: API_KEY_COLLECTION,
4514
+ id,
4515
+ data: {
4516
+ key: rawKey,
4517
+ keyPrefix: generateApiKeyPrefix(rawKey),
4518
+ lastUsedAt: null
4519
+ }
4520
+ });
4521
+ await sessionAuthAdapter?.createAuditLog({
4522
+ action: "api_key_rotate",
4523
+ userId: ctxUser.id,
4524
+ resource: "api_key",
4525
+ resourceId: id,
4526
+ success: true,
4527
+ metadata: { keyName: existing.name }
4528
+ });
4529
+ return c.json({
4530
+ ...updated,
4531
+ key: rawKey,
4532
+ permissions: existing.permissions,
4533
+ expiresAt: existing.expiresAt
4534
+ });
4535
+ } catch (error) {
4536
+ console.error("[ApiKeys] rotate error:", error);
4537
+ return c.json({ error: error.message }, 500);
4538
+ }
4539
+ });
4540
+ app.get("/api/webhooks", async (c) => {
4541
+ try {
4542
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4543
+ if (!ctxUser || !hasPermission(ctxUser, "users:read")) {
4544
+ return c.json({ error: "Forbidden" }, 403);
4545
+ }
4546
+ if (!webhookService) return c.json({ error: "Webhook service not available" }, 503);
4547
+ const webhooks = await webhookService.getWebhooks();
4548
+ return c.json({ docs: webhooks });
4549
+ } catch (error) {
4550
+ console.error("[Webhooks] GET error:", error);
4551
+ return c.json({ error: error.message }, 500);
4552
+ }
4553
+ });
4554
+ app.post("/api/webhooks", async (c) => {
4555
+ try {
4556
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4557
+ if (!ctxUser || !hasPermission(ctxUser, "users:admin")) {
4558
+ return c.json({ error: "Forbidden" }, 403);
4559
+ }
4560
+ if (!webhookService) return c.json({ error: "Webhook service not available" }, 503);
4561
+ const body = await c.req.json();
4562
+ const webhook = await webhookService.createWebhook(body);
4563
+ await sessionAuthAdapter?.createAuditLog({
4564
+ action: "webhook_create",
4565
+ userId: ctxUser.id,
4566
+ resource: "webhook",
4567
+ resourceId: webhook.id,
4568
+ success: true,
4569
+ metadata: { name: webhook.name, url: webhook.url }
4570
+ });
4571
+ return c.json(webhook, 201);
4572
+ } catch (error) {
4573
+ console.error("[Webhooks] POST error:", error);
4574
+ return c.json({ error: error.message }, 500);
4575
+ }
4576
+ });
4577
+ app.get("/api/webhooks/:id", async (c) => {
4578
+ try {
4579
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4580
+ if (!ctxUser || !hasPermission(ctxUser, "users:read")) {
4581
+ return c.json({ error: "Forbidden" }, 403);
4582
+ }
4583
+ if (!webhookService) return c.json({ error: "Webhook service not available" }, 503);
4584
+ const id = c.req.param("id");
4585
+ const webhook = await webhookService.getWebhookById(id);
4586
+ if (!webhook) return c.json({ error: "Webhook not found" }, 404);
4587
+ return c.json(webhook);
4588
+ } catch (error) {
4589
+ console.error("[Webhooks] GET :id error:", error);
4590
+ return c.json({ error: error.message }, 500);
4591
+ }
4592
+ });
4593
+ app.patch("/api/webhooks/:id", async (c) => {
4594
+ try {
4595
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4596
+ if (!ctxUser || !hasPermission(ctxUser, "users:admin")) {
4597
+ return c.json({ error: "Forbidden" }, 403);
4598
+ }
4599
+ if (!webhookService) return c.json({ error: "Webhook service not available" }, 503);
4600
+ const id = c.req.param("id");
4601
+ const body = await c.req.json();
4602
+ const updated = await webhookService.updateWebhook(id, body);
4603
+ if (!updated) return c.json({ error: "Webhook not found" }, 404);
4604
+ await sessionAuthAdapter?.createAuditLog({
4605
+ action: "webhook_update",
4606
+ userId: ctxUser.id,
4607
+ resource: "webhook",
4608
+ resourceId: id,
4609
+ success: true,
4610
+ metadata: { name: updated.name }
4611
+ });
4612
+ return c.json(updated);
4613
+ } catch (error) {
4614
+ console.error("[Webhooks] PATCH error:", error);
4615
+ return c.json({ error: error.message }, 500);
4616
+ }
4617
+ });
4618
+ app.delete("/api/webhooks/:id", async (c) => {
4619
+ try {
4620
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4621
+ if (!ctxUser || !hasPermission(ctxUser, "users:admin")) {
4622
+ return c.json({ error: "Forbidden" }, 403);
4623
+ }
4624
+ if (!webhookService) return c.json({ error: "Webhook service not available" }, 503);
4625
+ const id = c.req.param("id");
4626
+ const existing = await webhookService.getWebhookById(id);
4627
+ if (!existing) return c.json({ error: "Webhook not found" }, 404);
4628
+ await webhookService.deleteWebhook(id);
4629
+ await sessionAuthAdapter?.createAuditLog({
4630
+ action: "webhook_delete",
4631
+ userId: ctxUser.id,
4632
+ resource: "webhook",
4633
+ resourceId: id,
4634
+ success: true,
4635
+ metadata: { name: existing.name }
4636
+ });
4637
+ return c.json({ message: "Webhook deleted" });
4638
+ } catch (error) {
4639
+ console.error("[Webhooks] DELETE error:", error);
4640
+ return c.json({ error: error.message }, 500);
4641
+ }
4642
+ });
4643
+ app.post("/api/webhooks/:id/test", async (c) => {
4644
+ try {
4645
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4646
+ if (!ctxUser || !hasPermission(ctxUser, "users:admin")) {
4647
+ return c.json({ error: "Forbidden" }, 403);
4648
+ }
4649
+ if (!webhookService) return c.json({ error: "Webhook service not available" }, 503);
4650
+ const id = c.req.param("id");
4651
+ const result = await webhookService.testWebhook(id);
4652
+ if (!result) return c.json({ error: "Webhook not found" }, 404);
4653
+ return c.json(result);
4654
+ } catch (error) {
4655
+ console.error("[Webhooks] test error:", error);
4656
+ return c.json({ error: error.message }, 500);
4657
+ }
4658
+ });
4659
+ app.get("/api/webhooks/:id/history", async (c) => {
4660
+ try {
4661
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4662
+ if (!ctxUser || !hasPermission(ctxUser, "users:read")) {
4663
+ return c.json({ error: "Forbidden" }, 403);
4664
+ }
4665
+ if (!webhookService) return c.json({ error: "Webhook service not available" }, 503);
4666
+ const id = c.req.param("id");
4667
+ const limit = Math.min(parseInt(c.req.query("limit") || "50"), 100);
4668
+ const history = await webhookService.getDeliveryHistory(id, limit);
4669
+ return c.json({ docs: history });
4670
+ } catch (error) {
4671
+ console.error("[Webhooks] history error:", error);
4672
+ return c.json({ error: error.message }, 500);
4673
+ }
4674
+ });
4392
4675
  const collections = registry.getCollections();
4393
4676
  for (const collection of collections) {
4394
4677
  let computeDiff2 = function(a, b) {
@@ -4425,6 +4708,7 @@ function createHonoApp(options) {
4425
4708
  if (!access.allowed) {
4426
4709
  return c.json({ error: access.error }, access.status || 403);
4427
4710
  }
4711
+ auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, basePath, "GET", c.req.raw);
4428
4712
  const url = new URL(c.req.url);
4429
4713
  const page = parseInt(url.searchParams.get("page") || "1");
4430
4714
  const limit = Math.min(
@@ -4482,6 +4766,7 @@ function createHonoApp(options) {
4482
4766
  const url = new URL(c.req.url);
4483
4767
  const compareA = url.searchParams.get("compareA");
4484
4768
  const compareB = url.searchParams.get("compareB");
4769
+ auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, `${basePath}/${id}/versions`, "GET", c.req.raw);
4485
4770
  if (compareA && compareB) {
4486
4771
  const [versionA, versionB] = await Promise.all([
4487
4772
  db.findVersionByID({ collection: slug, versionId: compareA, tenantID: ctxTenantID }),
@@ -4494,7 +4779,7 @@ function createHonoApp(options) {
4494
4779
  return c.json({ diffs });
4495
4780
  }
4496
4781
  const page = parseInt(url.searchParams.get("page") || "1");
4497
- const limit = parseInt(url.searchParams.get("limit") || "10");
4782
+ const limit = Math.min(parseInt(url.searchParams.get("limit") || "10"), 100);
4498
4783
  const result = await db.findVersions({
4499
4784
  collection: slug,
4500
4785
  documentId: id,
@@ -4681,6 +4966,7 @@ function createHonoApp(options) {
4681
4966
  if (!access.allowed) {
4682
4967
  return c.json({ error: access.error }, access.status || 403);
4683
4968
  }
4969
+ auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, basePath, "POST", c.req.raw);
4684
4970
  const body = await c.req.json();
4685
4971
  const schema = registry.getCreateZodSchema(slug);
4686
4972
  let validated;
@@ -4740,13 +5026,19 @@ function createHonoApp(options) {
4740
5026
  const id = c.req.param("id");
4741
5027
  const body = await c.req.json();
4742
5028
  const baseUpdatedAt = readBaseUpdatedAt(body);
5029
+ console.log(`[PATCH] ${slug}/${id}`, {
5030
+ baseUpdatedAt,
5031
+ bodyKeys: Object.keys(body),
5032
+ tenantID: ctxTenantID
5033
+ });
4743
5034
  const cleaned = Object.fromEntries(
4744
5035
  Object.entries(omitRevisionFields(body)).filter(
4745
- ([_, v]) => v !== null && v !== "null" && v !== void 0
5036
+ ([_, v]) => v !== "null" && v !== void 0
4746
5037
  )
4747
5038
  );
4748
5039
  const schema = registry.getUpdateZodSchema(slug);
4749
5040
  const validated = schema.parse(cleaned);
5041
+ console.log(`[PATCH] Validated data:`, Object.keys(validated));
4750
5042
  const originalDoc = await db.findByID({
4751
5043
  collection: slug,
4752
5044
  id,
@@ -4754,6 +5046,9 @@ function createHonoApp(options) {
4754
5046
  draft: true
4755
5047
  // Always fetch current doc regardless of status
4756
5048
  });
5049
+ if (originalDoc) {
5050
+ console.log(`[PATCH] Original doc updatedAt:`, originalDoc.updatedAt);
5051
+ }
4757
5052
  if (!originalDoc) {
4758
5053
  return c.json({ error: "Document not found" }, 404);
4759
5054
  }
@@ -4773,7 +5068,7 @@ function createHonoApp(options) {
4773
5068
  changeDescription: "Manual save (Draft)",
4774
5069
  tenantID: ctxTenantID
4775
5070
  });
4776
- doc = await db.update({
5071
+ await db.update({
4777
5072
  collection: slug,
4778
5073
  id,
4779
5074
  data: { _has_draft: true },
@@ -4782,13 +5077,21 @@ function createHonoApp(options) {
4782
5077
  });
4783
5078
  } else {
4784
5079
  const saveData = isDraftEnabled ? { ...validated, _status: "draft", _has_draft: false } : validated;
4785
- doc = await db.update({
5080
+ await db.update({
4786
5081
  collection: slug,
4787
5082
  id,
4788
5083
  data: saveData,
4789
5084
  tenantID: ctxTenantID
4790
5085
  });
4791
- if (isDraftEnabled) {
5086
+ }
5087
+ doc = await db.findByID({
5088
+ collection: slug,
5089
+ id,
5090
+ tenantID: ctxTenantID,
5091
+ draft: true
5092
+ });
5093
+ if (isDraftEnabled) {
5094
+ if (!isAlreadyPublished) {
4792
5095
  await db.createVersion({
4793
5096
  collection: slug,
4794
5097
  documentId: id,
@@ -4798,13 +5101,13 @@ function createHonoApp(options) {
4798
5101
  changeDescription: "Manual save",
4799
5102
  tenantID: ctxTenantID
4800
5103
  });
4801
- } else {
4802
- await db.deleteDraft({
4803
- collection: slug,
4804
- documentId: id,
4805
- tenantID: ctxTenantID
4806
- });
4807
5104
  }
5105
+ } else {
5106
+ await db.deleteDraft({
5107
+ collection: slug,
5108
+ documentId: id,
5109
+ tenantID: ctxTenantID
5110
+ });
4808
5111
  }
4809
5112
  if (webhookService) {
4810
5113
  webhookService.trigger(getWebhookEvent(slug, "update"), {
@@ -4816,6 +5119,7 @@ function createHonoApp(options) {
4816
5119
  tenantId: ctxTenantID
4817
5120
  }).catch((err) => console.error(`[Webhook] Failed to trigger:`, err));
4818
5121
  }
5122
+ console.log(`[PATCH] Result doc updatedAt:`, doc?.updatedAt);
4819
5123
  return c.json({ data: doc, message: isDraftEnabled ? "Draft saved" : "Updated successfully" });
4820
5124
  } catch (error) {
4821
5125
  if (error.name === "ZodError") {
@@ -5510,5 +5814,5 @@ function createRESTAPI(registry, db, options) {
5510
5814
  }
5511
5815
 
5512
5816
  export { AuditLogger, AuthRoutes, InMemoryAuditLogger, InMemoryRateLimiter, MediaService, createAuditContext2 as createAuditContext, createHonoApp, createLocalStorage, createRESTAPI, getAppSecret, getEncryptionKey, getSessionConfig, init_secret, loadSecrets, resolveProvider, setDbAdapter };
5513
- //# sourceMappingURL=chunk-2SJATAN4.js.map
5514
- //# sourceMappingURL=chunk-2SJATAN4.js.map
5817
+ //# sourceMappingURL=chunk-OJBK3JYF.js.map
5818
+ //# sourceMappingURL=chunk-OJBK3JYF.js.map