@classytic/arc 2.22.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.
- package/dist/{BaseController-C6FlRWxF.mjs → BaseController--M8EMzt0.mjs} +92 -22
- package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
- package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
- package/dist/actionPermissions-CAphcmUG.mjs +24 -0
- package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
- package/dist/audit/index.d.mts +32 -3
- package/dist/audit/index.mjs +7 -4
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +264 -260
- package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
- package/dist/cache/index.d.mts +41 -3
- package/dist/cache/index.mjs +33 -4
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/context/index.mjs +2 -64
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-b6fgFgnd.mjs → core-EUNu0u07.mjs} +298 -12
- package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-B6GvAuAX.mjs} +4 -3
- package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
- package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
- package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
- package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
- package/dist/events/index.d.mts +57 -314
- package/dist/events/index.mjs +168 -93
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +24 -2
- package/dist/factory/index.d.mts +21 -6
- package/dist/factory/index.mjs +58 -18
- package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
- package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
- package/dist/headers-BvlQSDMB.mjs +45 -0
- package/dist/health-CB_m4S32.mjs +377 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +7 -0
- package/dist/idempotency/index.mjs +32 -20
- package/dist/{index-ogIHvOjo.d.mts → index-BMnbzvtO.d.mts} +70 -4
- package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
- package/dist/{index-DbtgCOCj.d.mts → index-DPRH0Cme.d.mts} +46 -94
- package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
- package/dist/{index-D8MKYGDa.d.mts → index-f4PTrk3B.d.mts} +37 -5
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +6 -5
- package/dist/{init-DleK3p9f.mjs → init-tlRSt667.mjs} +206 -205
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/jobs/index.d.mts +2 -0
- package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
- package/dist/integrations/mcp/index.d.mts +68 -5
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +149 -22
- package/dist/integrations/webhooks.mjs +186 -42
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +41 -14
- package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
- package/dist/interface-n5hpKg1d.mjs +16 -0
- package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
- package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-Cb7olw0j.mjs} +316 -45
- package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
- package/dist/middleware/index.d.mts +17 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/migrations/index.d.mts +94 -44
- package/dist/migrations/index.mjs +196 -44
- package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
- package/dist/{openapi-BRpMZD7C.mjs → openapi-DrLtog_B.mjs} +4 -4
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +4 -3
- package/dist/{permissions-BJMuKMl7.mjs → permissions-BrtiBKGs.mjs} +305 -492
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{actionPermissions-ze70xXJi.mjs → pipeline-BN5G05rd.mjs} +4 -27
- package/dist/plugins/index.d.mts +7 -78
- package/dist/plugins/index.mjs +14 -97
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{index-BEu4wjbL.d.mts → preloadResources-0oe-JSSS.d.mts} +59 -50
- package/dist/{createApp-Ci7yG5gv.mjs → preloadResources-DfyngIN9.mjs} +240 -68
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +3 -3
- package/dist/presets/index.d.mts +2 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-gw0ciIUD.mjs → presets-DwFDVKP0.mjs} +2 -2
- package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
- package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
- package/dist/{registerRoute-xHc42fwB.mjs → rateLimit-DrED-C4v.mjs} +2 -109
- package/dist/realtime-Cuw_deA-.mjs +375 -0
- package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
- package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/renewingLease-3cuQH7h8.mjs +44 -0
- package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
- package/dist/schedules-CjOd2MG9.mjs +113 -0
- package/dist/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
- package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
- package/dist/schemas/index.d.mts +67 -67
- package/dist/schemas/index.mjs +2 -3
- package/dist/scim/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +4 -4
- package/dist/scope-DfaPntpo.mjs +36 -0
- package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
- package/dist/{sse-CfUjwjDk.mjs → sse-BQ1w0dfl.mjs} +8 -31
- package/dist/sseStream-BQ7Qaynp.mjs +57 -0
- package/dist/testing/index.d.mts +4 -3
- package/dist/testing/index.mjs +2 -2
- package/dist/transactionContext-PNdoyLLm.mjs +64 -0
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
- package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
- package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
- package/dist/{types-Bc42CUyk.d.mts → types-QFGc3Cjv.d.mts} +37 -2
- package/dist/usage/index.d.mts +3 -52
- package/dist/usage/index.mjs +0 -0
- package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
- package/dist/utils/index.d.mts +90 -57
- package/dist/utils/index.mjs +7 -3
- package/dist/{utils-FhJ88TJE.mjs → utils-taUVD4p3.mjs} +47 -385
- package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
- package/dist/{versioning-DLwklPlG.d.mts → versioning-DXgY5BUm.d.mts} +52 -31
- package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
- package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
- package/package.json +526 -453
- package/skills/arc/references/api-reference.md +3 -1
- package/skills/arc/references/integrations.md +20 -11
- package/skills/arc/references/production.md +734 -696
- package/dist/QueryCache-Z1B38koh.d.mts +0 -40
- package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
- package/dist/health-ScyjIVo5.mjs +0 -202
- package/dist/requestId-5BhTVzBv.mjs +0 -113
- package/dist/scope-k15RXfcq.mjs +0 -64
- package/dist/types-XTApa8b6.mjs +0 -24
package/dist/auth/index.mjs
CHANGED
|
@@ -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 {
|
|
3
|
-
import { n as
|
|
4
|
-
import { o as toJsonSchema } from "../schemaConverter-
|
|
2
|
+
import { n as requireSingleHeaderValue } from "../headers-BvlQSDMB.mjs";
|
|
3
|
+
import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-Cg_y3fBz.mjs";
|
|
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
|
|
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
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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-
|
|
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,
|
package/dist/cache/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as CacheStats, r as CacheStore, t as CacheLogger } from "../interface-
|
|
2
|
-
import { a as QueryCacheConfig, i as QueryCache, n as CacheResult, r as CacheStatus, t as CacheEnvelope } from "../QueryCache-
|
|
3
|
-
import { i as queryCachePlugin, n as QueryCacheDefaults, r as QueryCachePluginOptions, t as CrossResourceRule } from "../queryCachePlugin-
|
|
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`.
|