@classytic/arc 2.23.0 → 2.25.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 (132) hide show
  1. package/dist/{BaseController-BQ8KNvWC.mjs → BaseController--M8EMzt0.mjs} +90 -21
  2. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  3. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  4. package/dist/audit/index.d.mts +32 -3
  5. package/dist/audit/index.mjs +7 -4
  6. package/dist/auth/index.d.mts +2 -2
  7. package/dist/auth/index.mjs +263 -259
  8. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  9. package/dist/cache/index.d.mts +41 -3
  10. package/dist/cache/index.mjs +33 -4
  11. package/dist/cli/commands/describe.d.mts +1 -1
  12. package/dist/cli/commands/docs.mjs +1 -1
  13. package/dist/cli/commands/generate.mjs +1 -1
  14. package/dist/cli/commands/init.mjs +1 -1
  15. package/dist/cli/index.mjs +2 -2
  16. package/dist/context/index.mjs +2 -64
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -4
  19. package/dist/{core-DQ6rDTyH.mjs → core-EUNu0u07.mjs} +296 -11
  20. package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-B6GvAuAX.mjs} +3 -2
  21. package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  25. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  26. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  27. package/dist/events/index.d.mts +57 -314
  28. package/dist/events/index.mjs +163 -91
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  31. package/dist/factory/index.d.mts +4 -5
  32. package/dist/factory/index.mjs +28 -8
  33. package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
  34. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  35. package/dist/headers-BvlQSDMB.mjs +45 -0
  36. package/dist/health-CB_m4S32.mjs +377 -0
  37. package/dist/hooks/index.d.mts +1 -1
  38. package/dist/idempotency/index.mjs +27 -3
  39. package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
  40. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  41. package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
  42. package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
  43. package/dist/{index-CC5ruDGh.d.mts → index-f4PTrk3B.d.mts} +37 -5
  44. package/dist/index.d.mts +6 -6
  45. package/dist/index.mjs +5 -5
  46. package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
  47. package/dist/integrations/event-gateway.d.mts +1 -1
  48. package/dist/integrations/event-gateway.mjs +1 -1
  49. package/dist/integrations/index.d.mts +3 -3
  50. package/dist/integrations/jobs/index.d.mts +2 -0
  51. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  52. package/dist/integrations/mcp/index.d.mts +4 -4
  53. package/dist/integrations/mcp/index.mjs +2 -2
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/integrations/webhooks.d.mts +37 -30
  57. package/dist/integrations/webhooks.mjs +51 -62
  58. package/dist/integrations/websocket-redis.d.mts +1 -1
  59. package/dist/integrations/websocket.d.mts +1 -1
  60. package/dist/integrations/websocket.mjs +41 -14
  61. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  62. package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cb7olw0j.mjs} +11 -9
  63. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  64. package/dist/middleware/index.d.mts +17 -1
  65. package/dist/middleware/index.mjs +1 -1
  66. package/dist/migrations/index.d.mts +94 -44
  67. package/dist/migrations/index.mjs +196 -44
  68. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  69. package/dist/{openapi-jSDx-VDv.mjs → openapi-DrLtog_B.mjs} +3 -3
  70. package/dist/permissions/index.d.mts +2 -2
  71. package/dist/permissions/index.mjs +2 -2
  72. package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
  73. package/dist/pipeline/index.d.mts +1 -1
  74. package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BN5G05rd.mjs} +3 -26
  75. package/dist/plugins/index.d.mts +6 -77
  76. package/dist/plugins/index.mjs +11 -95
  77. package/dist/plugins/response-cache.mjs +1 -0
  78. package/dist/plugins/tracing-entry.mjs +1 -1
  79. package/dist/{index-Ba10fdKp.d.mts → preloadResources-0oe-JSSS.d.mts} +50 -50
  80. package/dist/{createApp-CjOeWJ9_.mjs → preloadResources-DfyngIN9.mjs} +233 -70
  81. package/dist/presets/filesUpload.d.mts +3 -3
  82. package/dist/presets/filesUpload.mjs +3 -3
  83. package/dist/presets/index.d.mts +2 -1
  84. package/dist/presets/index.mjs +1 -1
  85. package/dist/presets/multiTenant.d.mts +1 -1
  86. package/dist/presets/multiTenant.mjs +1 -1
  87. package/dist/presets/search.d.mts +2 -2
  88. package/dist/presets/search.mjs +1 -1
  89. package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
  90. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  91. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  92. package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  93. package/dist/{realtime-I8OrfnMR.mjs → realtime-Cuw_deA-.mjs} +2 -2
  94. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  95. package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
  96. package/dist/registry/index.d.mts +2 -2
  97. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  98. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  99. package/dist/schedules-CjOd2MG9.mjs +113 -0
  100. package/dist/schemas/index.d.mts +67 -67
  101. package/dist/schemas/index.mjs +2 -3
  102. package/dist/scim/index.mjs +1 -2
  103. package/dist/scope/index.d.mts +2 -2
  104. package/dist/scope/index.mjs +4 -4
  105. package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
  106. package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
  107. package/dist/testing/index.d.mts +4 -3
  108. package/dist/testing/index.mjs +2 -2
  109. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  110. package/dist/types/index.d.mts +4 -4
  111. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  112. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  113. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  114. package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
  115. package/dist/usage/index.d.mts +3 -52
  116. package/dist/usage/index.mjs +0 -0
  117. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  118. package/dist/utils/index.d.mts +58 -85
  119. package/dist/utils/index.mjs +4 -2
  120. package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
  121. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  122. package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
  123. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  124. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  125. package/package.json +526 -513
  126. package/skills/arc/references/api-reference.md +3 -1
  127. package/skills/arc/references/integrations.md +20 -11
  128. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  129. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  130. package/dist/health-ScyjIVo5.mjs +0 -202
  131. package/dist/requestId-5BhTVzBv.mjs +0 -113
  132. package/dist/types-XTApa8b6.mjs +0 -24
@@ -1,7 +1,8 @@
1
1
  import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-DkO8Nk6-.mjs";
2
- import { n as normalizeRoles, t as getUserRoles } from "../types-XTApa8b6.mjs";
3
- import { _ as requireOrgMembership, v as requireOrgRole, x as requireTeamMembership } from "../permissions-tbxFHxjZ.mjs";
2
+ import { n as requireSingleHeaderValue } from "../headers-BvlQSDMB.mjs";
3
+ import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-Cg_y3fBz.mjs";
4
4
  import { o as toJsonSchema } from "../schemaConverter-DJvodiHj.mjs";
5
+ import { _ as requireOrgMembership, v as requireOrgRole, x as requireTeamMembership } from "../permissions-BrtiBKGs.mjs";
5
6
  import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
6
7
  import fp from "fastify-plugin";
7
8
  //#region src/auth/authPlugin.ts
@@ -289,6 +290,253 @@ var authPlugin_default = fp(authPlugin, {
289
290
  fastify: "5.x"
290
291
  });
291
292
  //#endregion
293
+ //#region src/auth/betterAuthOpenApi.ts
294
+ /**
295
+ * Check if a value looks like a Better Auth endpoint (has .path and .options)
296
+ */
297
+ function isBetterAuthEndpoint(value) {
298
+ if (typeof value !== "function" && typeof value !== "object") return false;
299
+ if (!value) return false;
300
+ const v = value;
301
+ return typeof v.path === "string" && typeof v.options === "object" && v.options !== null;
302
+ }
303
+ /**
304
+ * Convert Fastify-style path params (/:id) to OpenAPI-style (/{id})
305
+ */
306
+ function toOpenApiPath(path) {
307
+ return path.replace(/:([^/]+)/g, "{$1}");
308
+ }
309
+ /**
310
+ * Extract path parameters from a path string
311
+ */
312
+ function extractPathParams(path) {
313
+ const params = [];
314
+ const matches = path.matchAll(/:(\w+)/g);
315
+ for (const match of matches) {
316
+ const name = match[1];
317
+ if (!name) continue;
318
+ params.push({
319
+ name,
320
+ in: "path",
321
+ required: true,
322
+ schema: { type: "string" }
323
+ });
324
+ }
325
+ return params;
326
+ }
327
+ /**
328
+ * Extract OpenAPI paths from a Better Auth instance's API object.
329
+ *
330
+ * Walks `authApi` (the `auth.api` object from Better Auth), discovers
331
+ * endpoints, converts their Zod schemas to JSON Schema via `z.toJSONSchema()`,
332
+ * and returns a complete `ExternalOpenApiPaths` object ready for Arc's spec builder.
333
+ */
334
+ function extractBetterAuthOpenApi(authApi, options = {}) {
335
+ const { basePath = "/api/auth", tagName = "Authentication", tagDescription = "Better Auth authentication endpoints", excludePaths = [], excludeServerOnly = true, userFields } = options;
336
+ const normalizedBase = basePath.replace(/\/+$/, "");
337
+ const paths = {};
338
+ const detectedPlugins = detectActivePlugins(authApi);
339
+ const securityOptions = [{ cookieAuth: [] }, { bearerAuth: [] }];
340
+ if (detectedPlugins.apiKey) securityOptions.push({ apiKeyAuth: [] });
341
+ for (const [key, value] of Object.entries(authApi)) {
342
+ if (!isBetterAuthEndpoint(value)) continue;
343
+ const { path: endpointPath, options: endpointOpts } = value;
344
+ if (excludePaths.includes(endpointPath)) continue;
345
+ if (excludeServerOnly && endpointOpts.metadata?.SERVER_ONLY) continue;
346
+ const fullPath = toOpenApiPath(`${normalizedBase}${endpointPath}`);
347
+ const methods = [];
348
+ if (endpointOpts.method) if (Array.isArray(endpointOpts.method)) methods.push(...endpointOpts.method.map((m) => m.toLowerCase()));
349
+ else methods.push(endpointOpts.method.toLowerCase());
350
+ else methods.push(endpointOpts.body ? "post" : "get");
351
+ const openApiMeta = endpointOpts.metadata?.openapi;
352
+ for (const method of methods) {
353
+ const operation = {
354
+ tags: openApiMeta?.tags ?? [tagName],
355
+ operationId: openApiMeta?.operationId ?? key,
356
+ summary: openApiMeta?.summary ?? formatOperationSummary(key),
357
+ security: securityOptions
358
+ };
359
+ if (openApiMeta?.description) operation.description = openApiMeta.description;
360
+ const parameters = [...extractPathParams(endpointPath)];
361
+ if ((method === "get" || method === "delete") && endpointOpts.query) {
362
+ const querySchema = toJsonSchema(endpointOpts.query);
363
+ if (querySchema && querySchema.type === "object" && querySchema.properties) {
364
+ const props = querySchema.properties;
365
+ const required = querySchema.required ?? [];
366
+ for (const [name, prop] of Object.entries(props)) {
367
+ const paramEntry = {
368
+ name,
369
+ in: "query",
370
+ required: required.includes(name),
371
+ schema: prop
372
+ };
373
+ if (prop.description) paramEntry.description = prop.description;
374
+ parameters.push(paramEntry);
375
+ }
376
+ }
377
+ }
378
+ if (parameters.length > 0) operation.parameters = parameters;
379
+ if (method === "post" || method === "put" || method === "patch") {
380
+ if (openApiMeta?.requestBody) operation.requestBody = structuredClone(openApiMeta.requestBody);
381
+ else if (endpointOpts.body) {
382
+ const bodySchema = toJsonSchema(endpointOpts.body);
383
+ if (bodySchema) operation.requestBody = {
384
+ required: true,
385
+ content: { "application/json": { schema: bodySchema } }
386
+ };
387
+ }
388
+ if (userFields && isUserFieldEndpoint(endpointPath) && operation.requestBody) mergeUserFieldsIntoRequestBody(operation.requestBody, userFields, endpointPath);
389
+ }
390
+ if (openApiMeta?.responses) operation.responses = openApiMeta.responses;
391
+ else operation.responses = {
392
+ "200": { description: "Success" },
393
+ "400": { description: "Bad request" },
394
+ "401": { description: "Unauthorized" }
395
+ };
396
+ if (!paths[fullPath]) paths[fullPath] = {};
397
+ paths[fullPath][method] = operation;
398
+ }
399
+ }
400
+ const schemas = {
401
+ User: {
402
+ type: "object",
403
+ properties: {
404
+ id: { type: "string" },
405
+ name: { type: "string" },
406
+ email: {
407
+ type: "string",
408
+ format: "email"
409
+ },
410
+ emailVerified: { type: "boolean" },
411
+ image: {
412
+ type: "string",
413
+ nullable: true
414
+ },
415
+ createdAt: {
416
+ type: "string",
417
+ format: "date-time"
418
+ },
419
+ updatedAt: {
420
+ type: "string",
421
+ format: "date-time"
422
+ }
423
+ }
424
+ },
425
+ Session: {
426
+ type: "object",
427
+ properties: {
428
+ id: { type: "string" },
429
+ userId: { type: "string" },
430
+ token: { type: "string" },
431
+ expiresAt: {
432
+ type: "string",
433
+ format: "date-time"
434
+ },
435
+ ipAddress: {
436
+ type: "string",
437
+ nullable: true
438
+ },
439
+ userAgent: {
440
+ type: "string",
441
+ nullable: true
442
+ },
443
+ createdAt: {
444
+ type: "string",
445
+ format: "date-time"
446
+ },
447
+ updatedAt: {
448
+ type: "string",
449
+ format: "date-time"
450
+ }
451
+ }
452
+ }
453
+ };
454
+ if (userFields) {
455
+ const userProps = schemas.User?.properties;
456
+ for (const [name, field] of Object.entries(userFields)) {
457
+ const prop = { type: field.type };
458
+ if (field.description) prop.description = field.description;
459
+ userProps[name] = prop;
460
+ }
461
+ }
462
+ const securitySchemes = { cookieAuth: {
463
+ type: "apiKey",
464
+ in: "cookie",
465
+ name: "better-auth.session_token",
466
+ description: "Session cookie set by Better Auth after sign-in"
467
+ } };
468
+ if (detectedPlugins.apiKey) securitySchemes.apiKeyAuth = {
469
+ type: "apiKey",
470
+ in: "header",
471
+ name: "x-api-key",
472
+ description: "API key for programmatic access. Pass org context via x-organization-id header."
473
+ };
474
+ const resourceSecurity = [];
475
+ if (detectedPlugins.apiKey) resourceSecurity.push({
476
+ apiKeyAuth: [],
477
+ orgHeader: []
478
+ });
479
+ return {
480
+ paths,
481
+ schemas,
482
+ securitySchemes,
483
+ tags: [{
484
+ name: tagName,
485
+ description: tagDescription
486
+ }],
487
+ resourceSecurity: resourceSecurity.length > 0 ? resourceSecurity : void 0
488
+ };
489
+ }
490
+ /**
491
+ * Auto-detect active Better Auth plugins by inspecting the API object.
492
+ *
493
+ * Rather than hardcoding plugin-specific behavior, we check for known
494
+ * endpoint signatures that each plugin registers. This way the OpenAPI
495
+ * spec adapts automatically to whatever plugins the app has enabled —
496
+ * no Arc update needed when adding/removing plugins.
497
+ */
498
+ function detectActivePlugins(authApi) {
499
+ const endpointPaths = /* @__PURE__ */ new Set();
500
+ for (const value of Object.values(authApi)) if (isBetterAuthEndpoint(value)) endpointPaths.add(value.path);
501
+ return {
502
+ apiKey: endpointPaths.has("/api-key/create"),
503
+ organization: endpointPaths.has("/organization/create")
504
+ };
505
+ }
506
+ /**
507
+ * Convert a camelCase key like 'signInEmail' to a readable summary like 'Sign in email'
508
+ */
509
+ function formatOperationSummary(key) {
510
+ return key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
511
+ }
512
+ /**
513
+ * Check if an endpoint path should have userFields merged into its request body.
514
+ */
515
+ function isUserFieldEndpoint(path) {
516
+ return path === "/sign-up/email" || path === "/update-user";
517
+ }
518
+ /**
519
+ * Merge user-defined fields into an existing requestBody schema.
520
+ * For updateUser, all fields are treated as optional regardless of their `required` setting.
521
+ */
522
+ function mergeUserFieldsIntoRequestBody(requestBody, userFields, endpointPath) {
523
+ const content = requestBody?.content?.["application/json"];
524
+ if (!content?.schema) return;
525
+ const schema = content.schema;
526
+ if (!schema.properties) schema.properties = {};
527
+ if (!schema.required) schema.required = [];
528
+ const props = schema.properties;
529
+ const required = schema.required;
530
+ for (const [name, field] of Object.entries(userFields)) {
531
+ if (field.input === false) continue;
532
+ const isRequired = endpointPath === "/update-user" ? false : field.required ?? false;
533
+ const prop = { type: field.type };
534
+ if (field.description) prop.description = field.description;
535
+ props[name] = prop;
536
+ if (isRequired && !required.includes(name)) required.push(name);
537
+ }
538
+ }
539
+ //#endregion
292
540
  //#region src/auth/betterAuth.ts
293
541
  /**
294
542
  * Convert a Fastify request into a Fetch API Request.
@@ -532,7 +780,8 @@ function createBetterAuthAdapter(options) {
532
780
  };
533
781
  if (orgEnabled) {
534
782
  const session = sessionData.session;
535
- const activeOrgId = session?.activeOrganizationId || request.headers["x-organization-id"];
783
+ const requestedOrgId = requireSingleHeaderValue(request.headers, "x-organization-id");
784
+ const activeOrgId = session?.activeOrganizationId || requestedOrgId;
536
785
  if (activeOrgId) {
537
786
  let orgRoles = await getActiveMemberRoles(auth, headers);
538
787
  if (!orgRoles) orgRoles = await getMemberRolesByOrg(auth, headers, activeOrgId);
@@ -553,6 +802,7 @@ function createBetterAuthAdapter(options) {
553
802
  }
554
803
  }
555
804
  } catch (err) {
805
+ if (err instanceof ArcError) throw err;
556
806
  const message = exposeAuthErrors ? err instanceof Error ? err.message : String(err) : "Authentication required";
557
807
  reply.code(401).send({
558
808
  code: "arc.unauthorized",
@@ -586,7 +836,9 @@ function createBetterAuthAdapter(options) {
586
836
  userRoles: optUserRoles
587
837
  };
588
838
  if (orgEnabled) {
589
- const activeOrgId = sessionData.session?.activeOrganizationId || request.headers["x-organization-id"];
839
+ const session = sessionData.session;
840
+ const requestedOrgId = requireSingleHeaderValue(request.headers, "x-organization-id");
841
+ const activeOrgId = session?.activeOrganizationId || requestedOrgId;
590
842
  if (activeOrgId) {
591
843
  let orgRoles = await getActiveMemberRoles(auth, headers);
592
844
  if (!orgRoles) orgRoles = await getMemberRolesByOrg(auth, headers, activeOrgId);
@@ -599,7 +851,9 @@ function createBetterAuthAdapter(options) {
599
851
  };
600
852
  }
601
853
  }
602
- } catch {}
854
+ } catch (err) {
855
+ if (err instanceof ArcError) throw err;
856
+ }
603
857
  };
604
858
  let extractedOpenApi;
605
859
  if (openapiOpt === false) extractedOpenApi = void 0;
@@ -622,13 +876,10 @@ function createBetterAuthAdapter(options) {
622
876
  if (!fastify.hasDecorator("optionalAuthenticate")) fastify.decorate("optionalAuthenticate", optionalAuthenticate);
623
877
  if (!fastify.hasRequestDecorator("user")) fastify.decorateRequest("user", void 0);
624
878
  if (!fastify.hasRequestDecorator("session")) fastify.decorateRequest("session", void 0);
625
- if (!extractedOpenApi && openapiOpt !== false && auth.api && typeof auth.api === "object") {
626
- const { extractBetterAuthOpenApi } = await import("../betterAuthOpenApi-CV1lxpRX.mjs");
627
- extractedOpenApi = extractBetterAuthOpenApi(auth.api, {
628
- basePath,
629
- userFields
630
- });
631
- }
879
+ if (!extractedOpenApi && openapiOpt !== false && auth.api && typeof auth.api === "object") extractedOpenApi = extractBetterAuthOpenApi(auth.api, {
880
+ basePath,
881
+ userFields
882
+ });
632
883
  if (extractedOpenApi) {
633
884
  const arc = fastify.arc;
634
885
  if (arc?.externalOpenApiPaths) arc.externalOpenApiPaths.push(extractedOpenApi);
@@ -651,253 +902,6 @@ function createBetterAuthAdapter(options) {
651
902
  };
652
903
  }
653
904
  //#endregion
654
- //#region src/auth/betterAuthOpenApi.ts
655
- /**
656
- * Check if a value looks like a Better Auth endpoint (has .path and .options)
657
- */
658
- function isBetterAuthEndpoint(value) {
659
- if (typeof value !== "function" && typeof value !== "object") return false;
660
- if (!value) return false;
661
- const v = value;
662
- return typeof v.path === "string" && typeof v.options === "object" && v.options !== null;
663
- }
664
- /**
665
- * Convert Fastify-style path params (/:id) to OpenAPI-style (/{id})
666
- */
667
- function toOpenApiPath(path) {
668
- return path.replace(/:([^/]+)/g, "{$1}");
669
- }
670
- /**
671
- * Extract path parameters from a path string
672
- */
673
- function extractPathParams(path) {
674
- const params = [];
675
- const matches = path.matchAll(/:(\w+)/g);
676
- for (const match of matches) {
677
- const name = match[1];
678
- if (!name) continue;
679
- params.push({
680
- name,
681
- in: "path",
682
- required: true,
683
- schema: { type: "string" }
684
- });
685
- }
686
- return params;
687
- }
688
- /**
689
- * Extract OpenAPI paths from a Better Auth instance's API object.
690
- *
691
- * Walks `authApi` (the `auth.api` object from Better Auth), discovers
692
- * endpoints, converts their Zod schemas to JSON Schema via `z.toJSONSchema()`,
693
- * and returns a complete `ExternalOpenApiPaths` object ready for Arc's spec builder.
694
- */
695
- function extractBetterAuthOpenApi(authApi, options = {}) {
696
- const { basePath = "/api/auth", tagName = "Authentication", tagDescription = "Better Auth authentication endpoints", excludePaths = [], excludeServerOnly = true, userFields } = options;
697
- const normalizedBase = basePath.replace(/\/+$/, "");
698
- const paths = {};
699
- const detectedPlugins = detectActivePlugins(authApi);
700
- const securityOptions = [{ cookieAuth: [] }, { bearerAuth: [] }];
701
- if (detectedPlugins.apiKey) securityOptions.push({ apiKeyAuth: [] });
702
- for (const [key, value] of Object.entries(authApi)) {
703
- if (!isBetterAuthEndpoint(value)) continue;
704
- const { path: endpointPath, options: endpointOpts } = value;
705
- if (excludePaths.includes(endpointPath)) continue;
706
- if (excludeServerOnly && endpointOpts.metadata?.SERVER_ONLY) continue;
707
- const fullPath = toOpenApiPath(`${normalizedBase}${endpointPath}`);
708
- const methods = [];
709
- if (endpointOpts.method) if (Array.isArray(endpointOpts.method)) methods.push(...endpointOpts.method.map((m) => m.toLowerCase()));
710
- else methods.push(endpointOpts.method.toLowerCase());
711
- else methods.push(endpointOpts.body ? "post" : "get");
712
- const openApiMeta = endpointOpts.metadata?.openapi;
713
- for (const method of methods) {
714
- const operation = {
715
- tags: openApiMeta?.tags ?? [tagName],
716
- operationId: openApiMeta?.operationId ?? key,
717
- summary: openApiMeta?.summary ?? formatOperationSummary(key),
718
- security: securityOptions
719
- };
720
- if (openApiMeta?.description) operation.description = openApiMeta.description;
721
- const parameters = [...extractPathParams(endpointPath)];
722
- if ((method === "get" || method === "delete") && endpointOpts.query) {
723
- const querySchema = toJsonSchema(endpointOpts.query);
724
- if (querySchema && querySchema.type === "object" && querySchema.properties) {
725
- const props = querySchema.properties;
726
- const required = querySchema.required ?? [];
727
- for (const [name, prop] of Object.entries(props)) {
728
- const paramEntry = {
729
- name,
730
- in: "query",
731
- required: required.includes(name),
732
- schema: prop
733
- };
734
- if (prop.description) paramEntry.description = prop.description;
735
- parameters.push(paramEntry);
736
- }
737
- }
738
- }
739
- if (parameters.length > 0) operation.parameters = parameters;
740
- if (method === "post" || method === "put" || method === "patch") {
741
- if (openApiMeta?.requestBody) operation.requestBody = structuredClone(openApiMeta.requestBody);
742
- else if (endpointOpts.body) {
743
- const bodySchema = toJsonSchema(endpointOpts.body);
744
- if (bodySchema) operation.requestBody = {
745
- required: true,
746
- content: { "application/json": { schema: bodySchema } }
747
- };
748
- }
749
- if (userFields && isUserFieldEndpoint(endpointPath) && operation.requestBody) mergeUserFieldsIntoRequestBody(operation.requestBody, userFields, endpointPath);
750
- }
751
- if (openApiMeta?.responses) operation.responses = openApiMeta.responses;
752
- else operation.responses = {
753
- "200": { description: "Success" },
754
- "400": { description: "Bad request" },
755
- "401": { description: "Unauthorized" }
756
- };
757
- if (!paths[fullPath]) paths[fullPath] = {};
758
- paths[fullPath][method] = operation;
759
- }
760
- }
761
- const schemas = {
762
- User: {
763
- type: "object",
764
- properties: {
765
- id: { type: "string" },
766
- name: { type: "string" },
767
- email: {
768
- type: "string",
769
- format: "email"
770
- },
771
- emailVerified: { type: "boolean" },
772
- image: {
773
- type: "string",
774
- nullable: true
775
- },
776
- createdAt: {
777
- type: "string",
778
- format: "date-time"
779
- },
780
- updatedAt: {
781
- type: "string",
782
- format: "date-time"
783
- }
784
- }
785
- },
786
- Session: {
787
- type: "object",
788
- properties: {
789
- id: { type: "string" },
790
- userId: { type: "string" },
791
- token: { type: "string" },
792
- expiresAt: {
793
- type: "string",
794
- format: "date-time"
795
- },
796
- ipAddress: {
797
- type: "string",
798
- nullable: true
799
- },
800
- userAgent: {
801
- type: "string",
802
- nullable: true
803
- },
804
- createdAt: {
805
- type: "string",
806
- format: "date-time"
807
- },
808
- updatedAt: {
809
- type: "string",
810
- format: "date-time"
811
- }
812
- }
813
- }
814
- };
815
- if (userFields) {
816
- const userProps = schemas.User?.properties;
817
- for (const [name, field] of Object.entries(userFields)) {
818
- const prop = { type: field.type };
819
- if (field.description) prop.description = field.description;
820
- userProps[name] = prop;
821
- }
822
- }
823
- const securitySchemes = { cookieAuth: {
824
- type: "apiKey",
825
- in: "cookie",
826
- name: "better-auth.session_token",
827
- description: "Session cookie set by Better Auth after sign-in"
828
- } };
829
- if (detectedPlugins.apiKey) securitySchemes.apiKeyAuth = {
830
- type: "apiKey",
831
- in: "header",
832
- name: "x-api-key",
833
- description: "API key for programmatic access. Pass org context via x-organization-id header."
834
- };
835
- const resourceSecurity = [];
836
- if (detectedPlugins.apiKey) resourceSecurity.push({
837
- apiKeyAuth: [],
838
- orgHeader: []
839
- });
840
- return {
841
- paths,
842
- schemas,
843
- securitySchemes,
844
- tags: [{
845
- name: tagName,
846
- description: tagDescription
847
- }],
848
- resourceSecurity: resourceSecurity.length > 0 ? resourceSecurity : void 0
849
- };
850
- }
851
- /**
852
- * Auto-detect active Better Auth plugins by inspecting the API object.
853
- *
854
- * Rather than hardcoding plugin-specific behavior, we check for known
855
- * endpoint signatures that each plugin registers. This way the OpenAPI
856
- * spec adapts automatically to whatever plugins the app has enabled —
857
- * no Arc update needed when adding/removing plugins.
858
- */
859
- function detectActivePlugins(authApi) {
860
- const endpointPaths = /* @__PURE__ */ new Set();
861
- for (const value of Object.values(authApi)) if (isBetterAuthEndpoint(value)) endpointPaths.add(value.path);
862
- return {
863
- apiKey: endpointPaths.has("/api-key/create"),
864
- organization: endpointPaths.has("/organization/create")
865
- };
866
- }
867
- /**
868
- * Convert a camelCase key like 'signInEmail' to a readable summary like 'Sign in email'
869
- */
870
- function formatOperationSummary(key) {
871
- return key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
872
- }
873
- /**
874
- * Check if an endpoint path should have userFields merged into its request body.
875
- */
876
- function isUserFieldEndpoint(path) {
877
- return path === "/sign-up/email" || path === "/update-user";
878
- }
879
- /**
880
- * Merge user-defined fields into an existing requestBody schema.
881
- * For updateUser, all fields are treated as optional regardless of their `required` setting.
882
- */
883
- function mergeUserFieldsIntoRequestBody(requestBody, userFields, endpointPath) {
884
- const content = requestBody?.content?.["application/json"];
885
- if (!content?.schema) return;
886
- const schema = content.schema;
887
- if (!schema.properties) schema.properties = {};
888
- if (!schema.required) schema.required = [];
889
- const props = schema.properties;
890
- const required = schema.required;
891
- for (const [name, field] of Object.entries(userFields)) {
892
- if (field.input === false) continue;
893
- const isRequired = endpointPath === "/update-user" ? false : field.required ?? false;
894
- const prop = { type: field.type };
895
- if (field.description) prop.description = field.description;
896
- props[name] = prop;
897
- if (isRequired && !required.includes(name)) required.push(name);
898
- }
899
- }
900
- //#endregion
901
905
  //#region src/auth/sessionManager.ts
902
906
  /**
903
907
  * Session Management for Arc
@@ -1,4 +1,4 @@
1
- import { i as compileAggRequest, t as adapterSupportsAggregate } from "./validate-C2mGgSXg.mjs";
1
+ import { i as compileAggRequest, t as adapterSupportsAggregate } from "./validate-Bk44F2yT.mjs";
2
2
  //#region src/core/aggregation/buildHandler.ts
3
3
  /**
4
4
  * Framework-agnostic aggregation execution. Runs safety guards,
@@ -1,6 +1,6 @@
1
- import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-C1_WLTMK.mjs";
2
- import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-Z1B38koh.mjs";
3
- import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-CfYWq-Ak.mjs";
1
+ import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-BGZCzOGL.mjs";
2
+ import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-DOMT_iq7.mjs";
3
+ import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-br4otiqF.mjs";
4
4
  //#region src/cache/keys.d.ts
5
5
  /**
6
6
  * Cache Key Utilities
@@ -74,6 +74,18 @@ declare class MemoryCacheStore<TValue = unknown> implements CacheStore<TValue> {
74
74
  get(key: string): Promise<TValue | undefined>;
75
75
  set(key: string, value: TValue, ttlSeconds?: number): Promise<void>;
76
76
  delete(key: string): Promise<void>;
77
+ /**
78
+ * Atomic increment (canonical `CacheAdapter` signature: key, by, ttl) —
79
+ * synchronous on purpose: no `await` between read and write means
80
+ * concurrent bumps on the event loop can't interleave, which is the whole
81
+ * point of the `CacheStore.increment` contract.
82
+ *
83
+ * TTL semantics follow the repo-core contract: `ttlSeconds` applies only
84
+ * when the key is CREATED — an existing counter keeps its original expiry
85
+ * (Redis `EXPIRE ... NX` semantics), so every conforming adapter ages a
86
+ * counter identically.
87
+ */
88
+ increment(key: string, by?: number, ttlSeconds?: number): number;
77
89
  clear(pattern?: string): Promise<void>;
78
90
  close(): Promise<void>;
79
91
  stats(): CacheStats;
@@ -102,6 +114,16 @@ interface RedisCacheClient {
102
114
  scan?(cursor: string | number, ...args: (string | number)[]): Promise<[string | number, string[]]>;
103
115
  /** Optional: pipeline for batched commands (ioredis compatible) */
104
116
  pipeline?(): RedisPipeline;
117
+ /** Optional: atomic increment-by (Redis INCRBY). Enables `CacheStore.increment`. */
118
+ incrBy?(key: string, by: number): Promise<number>;
119
+ /**
120
+ * Optional: set a TTL only when the key has none (Redis 7 `EXPIRE ... NX`,
121
+ * or a Lua `TTL`/`EXPIRE` script on older servers). Required alongside
122
+ * `incrBy` for the atomic `increment` path — the CacheAdapter contract is
123
+ * TTL-ON-CREATE, and no return value of INCRBY can prove creation (an
124
+ * existing counter at 0 incremented by `by` also returns `by`).
125
+ */
126
+ expireIfAbsent?(key: string, seconds: number): Promise<unknown>;
105
127
  }
106
128
  interface RedisPipeline {
107
129
  del(key: string): unknown;
@@ -134,6 +156,18 @@ declare class RedisCacheStore<TValue = unknown> implements CacheStore<TValue> {
134
156
  get(key: string): Promise<TValue | undefined>;
135
157
  set(key: string, value: TValue, ttlSeconds?: number): Promise<void>;
136
158
  delete(key: string): Promise<void>;
159
+ /**
160
+ * Atomic increment (canonical `CacheAdapter` signature: key, by, ttl).
161
+ *
162
+ * The atomic path requires `incrBy` AND — when a TTL is requested —
163
+ * `expireIfAbsent`: TTL-on-create cannot be inferred from INCRBY's
164
+ * return value (an existing counter at 0 also returns `by`), so the
165
+ * NX-expiry must be its own atomic server-side operation. A client that
166
+ * can't honor the COMPLETE contract falls back to read-modify-write
167
+ * (monotonic, but a concurrent replica's bump can be lost, and the TTL
168
+ * refreshes per write since plain SET can't preserve expiry).
169
+ */
170
+ increment(key: string, by?: number, ttlSeconds?: number): Promise<number>;
137
171
  /**
138
172
  * Invalidate keys. Pass a glob pattern to delete a subset (`user:*:v2`);
139
173
  * omit to clear every key under this store's prefix.
@@ -156,6 +190,8 @@ interface IoredisLike {
156
190
  del(key: string): unknown;
157
191
  exec(): Promise<unknown>;
158
192
  };
193
+ incrby?(key: string, by: number): Promise<number>;
194
+ eval?(script: string, numKeys: number, ...args: (string | number)[]): Promise<unknown>;
159
195
  }
160
196
  /**
161
197
  * Wrap an ioredis instance as a `RedisCacheClient`.
@@ -193,6 +229,8 @@ interface UpstashRedisLike {
193
229
  match?: string;
194
230
  count?: number;
195
231
  }): Promise<[number, string[]] | [string, string[]]>;
232
+ incrby?(key: string, by: number): Promise<number>;
233
+ eval?(script: string, keys: string[], args: (string | number)[]): Promise<unknown>;
196
234
  }
197
235
  /**
198
236
  * Wrap an `@upstash/redis` REST client as a `RedisCacheClient`.