@classytic/arc 2.6.3 → 2.7.3
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/README.md +98 -3
- package/dist/{BaseController-DzRtluEF.mjs → BaseController-CpMfCXdn.mjs} +134 -16
- package/dist/adapters/index.d.mts +2 -2
- package/dist/adapters/index.mjs +1 -1
- package/dist/{adapters-gM-WYjNe.mjs → adapters-BxGgSHjj.mjs} +1 -9
- package/dist/applyPermissionResult-D6GPMsvh.mjs +37 -0
- package/dist/audit/index.d.mts +1 -1
- package/dist/audit/index.mjs +1 -1
- package/dist/audit/mongodb.d.mts +1 -1
- package/dist/audit/mongodb.mjs +1 -1
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +7 -6
- package/dist/auth/mongoose.d.mts +191 -0
- package/dist/auth/mongoose.mjs +73 -0
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-lz0IRbXJ.mjs → betterAuthOpenApi-CCw3YX0g.mjs} +1 -1
- package/dist/cache/index.d.mts +2 -2
- package/dist/cache/index.mjs +2 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +7 -5
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-C1XCMtqM.mjs → core-BWekSEju.mjs} +41 -13
- package/dist/{createApp-D2w0LdYJ.mjs → createApp-D7e77m8C.mjs} +25 -14
- package/dist/{defineResource-wWMBB4GP.mjs → defineResource-DZzyl4a4.mjs} +42 -37
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/dynamic/index.d.mts +2 -2
- package/dist/dynamic/index.mjs +2 -2
- package/dist/{elevation-BEdACOLB.mjs → elevation-By_p2lnn.mjs} +1 -1
- package/dist/elevation-D7WK0RXq.d.mts +23 -0
- package/dist/{errorHandler-r2595m8T.mjs → errorHandler-CH8wk1eD.mjs} +17 -2
- package/dist/{errorHandler-Do4vVQ1f.d.mts → errorHandler-pCpEtNd7.d.mts} +46 -2
- package/dist/{eventPlugin-Ba00swHF.mjs → eventPlugin-B6U_nCFU.mjs} +4 -3
- package/dist/{eventPlugin-DW45v4V5.d.mts → eventPlugin-CdvUoUna.d.mts} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +1 -1
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +3 -3
- package/dist/idempotency/mongodb.d.mts +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/index-B0extFr4.d.mts +640 -0
- package/dist/{index-gz6iuzCp.d.mts → index-BjShrzoj.d.mts} +47 -4
- package/dist/{index-CHeJa4Zd.d.mts → index-C9eYNjGR.d.mts} +1 -1
- package/dist/index.d.mts +9 -8
- package/dist/index.mjs +10 -9
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +8 -5
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +58 -1
- package/dist/integrations/webhooks.mjs +78 -7
- package/dist/integrations/websocket.d.mts +7 -1
- package/dist/integrations/websocket.mjs +7 -1
- package/dist/{interface-DYH8AXGe.d.mts → interface-B91alUzq.d.mts} +151 -15
- package/dist/{mongodb-pMvOlR5_.d.mts → mongodb-B7zupyck.d.mts} +1 -1
- package/dist/{mongodb-kltrBPa1.d.mts → mongodb-Cgu9F1Nd.d.mts} +1 -1
- package/dist/{openapi-CBmZ6EQN.mjs → openapi-BBSTVcMm.mjs} +1 -1
- package/dist/org/index.d.mts +2 -2
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +4 -4
- package/dist/permissions/index.mjs +3 -2
- package/dist/{permissions-C8ImI8gC.mjs → permissions-CH4cNwJi.mjs} +358 -64
- package/dist/plugins/index.d.mts +52 -5
- package/dist/plugins/index.mjs +12 -11
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/policies/index.d.mts +1 -1
- package/dist/presets/index.d.mts +3 -3
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +53 -3
- package/dist/presets/multiTenant.mjs +89 -47
- package/dist/{presets-BMfdy34e.mjs → presets-BFrGvvjL.mjs} +2 -2
- package/dist/{queryCachePlugin-DcmETvcB.d.mts → queryCachePlugin-Ckl71mkc.d.mts} +1 -1
- package/dist/{queryCachePlugin-XtFplYO9.mjs → queryCachePlugin-CwTpR04-.mjs} +2 -2
- package/dist/{redis-D0Qc-9EW.d.mts → redis-3TQxm2VZ.d.mts} +1 -1
- package/dist/{redis-stream-BW9UKLZM.d.mts → redis-stream-Dag5LFa9.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/replyHelpers-uDUIYh7u.mjs +40 -0
- package/dist/{resourceToTools-nCJWnG1r.mjs → resourceToTools-BJkoQoUP.mjs} +74 -25
- package/dist/rpc/index.d.mts +1 -1
- package/dist/rpc/index.mjs +1 -1
- package/dist/scope/index.d.mts +3 -2
- package/dist/scope/index.mjs +4 -3
- package/dist/{sse-BF7GR7IB.mjs → sse-6W0hjVS_.mjs} +2 -2
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +1 -1
- package/dist/types/index.d.mts +4 -3
- package/dist/types/index.mjs +1 -1
- package/dist/types--D3vvfdt.d.mts +286 -0
- package/dist/{types-By-5mIfn.d.mts → types-2FlNl0mL.d.mts} +44 -9
- package/dist/types-AOD8fxIw.mjs +229 -0
- package/dist/types-B4BNthET.d.mts +178 -0
- package/dist/{types-B4_TDdPe.d.mts → types-C5g2oRC7.d.mts} +18 -2
- package/dist/utils/index.d.mts +3 -3
- package/dist/utils/index.mjs +5 -5
- package/package.json +21 -6
- package/skills/arc/SKILL.md +314 -6
- package/skills/arc/references/integrations.md +32 -7
- package/skills/arc/references/mcp.md +31 -7
- package/skills/arc/references/multi-tenancy.md +208 -0
- package/skills/arc/references/production.md +69 -0
- package/dist/elevation-C_taLQrM.d.mts +0 -147
- package/dist/index-NGZksqM5.d.mts +0 -398
- package/dist/types-BNUccdcf.d.mts +0 -101
- package/dist/types-BhtYdxZU.mjs +0 -91
- /package/dist/{EventTransport-wc5hSLik.d.mts → EventTransport-C4VheKeC.d.mts} +0 -0
- /package/dist/{HookSystem-COkyWztM.mjs → HookSystem-D7lfx--K.mjs} +0 -0
- /package/dist/{ResourceRegistry-C6ngvOnn.mjs → ResourceRegistry-DsHiG9cL.mjs} +0 -0
- /package/dist/{caching-BSXB-Xr7.mjs → caching-5DtLwIqb.mjs} +0 -0
- /package/dist/{circuitBreaker-JP2GdJ4b.d.mts → circuitBreaker-BBPDt-J_.d.mts} +0 -0
- /package/dist/{circuitBreaker-BOBOpN2w.mjs → circuitBreaker-l18oRgL5.mjs} +0 -0
- /package/dist/{errors-CcVbl1-T.d.mts → errors-BS6lZvWy.d.mts} +0 -0
- /package/dist/{errors-NoQKsbAT.mjs → errors-Cg58SLNi.mjs} +0 -0
- /package/dist/{externalPaths-DpO-s7r8.d.mts → externalPaths-iba7jD3d.d.mts} +0 -0
- /package/dist/{fields-DFwdaWCq.d.mts → fields-D4nMDqnK.d.mts} +0 -0
- /package/dist/{interface-D_BWALyZ.d.mts → interface-CG7oRZjX.d.mts} +0 -0
- /package/dist/{interface-gr-7qo9j.d.mts → interface-CSbZdv_3.d.mts} +0 -0
- /package/dist/{logger-Dz3j1ItV.mjs → logger-DLg8-Ueg.mjs} +0 -0
- /package/dist/{memory-BFAYkf8H.mjs → memory-Cp7_cAko.mjs} +0 -0
- /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
- /package/dist/{mongodb-BuQ7fNTg.mjs → mongodb-B7X7P1P8.mjs} +0 -0
- /package/dist/{pluralize-CcT6qF0a.mjs → pluralize-Dckfq6US.mjs} +0 -0
- /package/dist/{registry-I-ogLgL9.mjs → registry-B3lRFBWo.mjs} +0 -0
- /package/dist/{requestContext-DYtmNpm5.mjs → requestContext-xHIKedG6.mjs} +0 -0
- /package/dist/{schemaConverter-DjzHpFam.mjs → schemaConverter-0TyONAwM.mjs} +0 -0
- /package/dist/{sessionManager-wbkYj2HL.d.mts → sessionManager-CEo9jwPI.d.mts} +0 -0
- /package/dist/{tracing-bz_U4EM1.d.mts → tracing-DEqdGkr-.d.mts} +0 -0
- /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
- /package/dist/{utils-Dc0WhlIl.mjs → utils-B-l6410F.mjs} +0 -0
- /package/dist/{versioning-BzfeHmhj.mjs → versioning-CdBbFefk.mjs} +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
//#region src/scope/types.ts
|
|
2
|
+
/** Check if scope is `member` kind */
|
|
3
|
+
function isMember(scope) {
|
|
4
|
+
return scope.kind === "member";
|
|
5
|
+
}
|
|
6
|
+
/** Check if scope is `elevated` kind */
|
|
7
|
+
function isElevated(scope) {
|
|
8
|
+
return scope.kind === "elevated";
|
|
9
|
+
}
|
|
10
|
+
/** Check if scope is `service` kind (machine-to-machine auth) */
|
|
11
|
+
function isService(scope) {
|
|
12
|
+
return scope.kind === "service";
|
|
13
|
+
}
|
|
14
|
+
/** Check if scope has org access (member, service, or elevated) */
|
|
15
|
+
function hasOrgAccess(scope) {
|
|
16
|
+
return scope.kind === "member" || scope.kind === "service" || scope.kind === "elevated";
|
|
17
|
+
}
|
|
18
|
+
/** Check if request is authenticated (any kind except public) */
|
|
19
|
+
function isAuthenticated(scope) {
|
|
20
|
+
return scope.kind !== "public";
|
|
21
|
+
}
|
|
22
|
+
/** Get organizationId from scope (member, service, or elevated — undefined otherwise) */
|
|
23
|
+
function getOrgId(scope) {
|
|
24
|
+
if (scope.kind === "member") return scope.organizationId;
|
|
25
|
+
if (scope.kind === "service") return scope.organizationId;
|
|
26
|
+
if (scope.kind === "elevated") return scope.organizationId;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get stable client identity from a service scope.
|
|
30
|
+
*
|
|
31
|
+
* Returns the `clientId` for machine-to-machine auth (API keys, service accounts),
|
|
32
|
+
* or `undefined` for any other scope kind. Use this for audit logging, rate limiting,
|
|
33
|
+
* and anywhere you need to distinguish "this specific API client" from "this user".
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const clientId = getClientId(request.scope);
|
|
38
|
+
* if (clientId) {
|
|
39
|
+
* auditLog.record({ actor: clientId, action: 'create' });
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
function getClientId(scope) {
|
|
44
|
+
if (scope.kind === "service") return scope.clientId;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get OAuth-style scope strings from a service scope (e.g. `['jobs:write']`).
|
|
48
|
+
* Returns an empty array for any non-service kind.
|
|
49
|
+
*/
|
|
50
|
+
function getServiceScopes(scope) {
|
|
51
|
+
if (scope.kind === "service") return scope.scopes ?? [];
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
/** Get org roles from scope (empty array if not a member) */
|
|
55
|
+
function getOrgRoles(scope) {
|
|
56
|
+
if (scope.kind === "member") return scope.orgRoles;
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
/** Get team ID from scope (only available on member kind) */
|
|
60
|
+
function getTeamId(scope) {
|
|
61
|
+
if (scope.kind === "member") return scope.teamId;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get an app-defined scope dimension by key (e.g. `branchId`, `projectId`).
|
|
65
|
+
*
|
|
66
|
+
* Returns the value when the scope is `member`/`service`/`elevated` AND has
|
|
67
|
+
* `context` set AND the key exists; `undefined` otherwise. Designed to be
|
|
68
|
+
* the single read path for any custom tenancy dimension your app cares about
|
|
69
|
+
* — branch, project, department, region, workspace, etc.
|
|
70
|
+
*
|
|
71
|
+
* Arc itself takes no position on what keys you use — that's your domain.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* import { getScopeContext } from '@classytic/arc/scope';
|
|
76
|
+
*
|
|
77
|
+
* const branchId = getScopeContext(request.scope, 'branchId');
|
|
78
|
+
* if (!branchId) return reply.code(403).send({ error: 'Branch context required' });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
function getScopeContext(scope, key) {
|
|
82
|
+
if (scope.kind === "member" || scope.kind === "service" || scope.kind === "elevated") return scope.context?.[key];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get the full scope context map (read-only). Returns `undefined` for scope
|
|
86
|
+
* kinds that don't carry context (`public`, `authenticated`).
|
|
87
|
+
*/
|
|
88
|
+
function getScopeContextMap(scope) {
|
|
89
|
+
if (scope.kind === "member" || scope.kind === "service" || scope.kind === "elevated") return scope.context;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get the parent-organization chain for a scope (closest-first, root-last).
|
|
93
|
+
*
|
|
94
|
+
* Returns the `ancestorOrgIds` array when the scope is `member`/`service`/
|
|
95
|
+
* `elevated` and has it set; an empty array otherwise (including for kinds
|
|
96
|
+
* that can't carry org context).
|
|
97
|
+
*
|
|
98
|
+
* Arc takes no position on what the chain represents — your auth function
|
|
99
|
+
* loads it from your own data model. Common use cases: holding company →
|
|
100
|
+
* subsidiaries, MSP → managed tenants, white-label parent → child accounts.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* import { getAncestorOrgIds } from '@classytic/arc/scope';
|
|
105
|
+
*
|
|
106
|
+
* const ancestors = getAncestorOrgIds(request.scope);
|
|
107
|
+
* if (ancestors.includes('acme-holding')) {
|
|
108
|
+
* // caller has access to a path that includes Acme Holding
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
function getAncestorOrgIds(scope) {
|
|
113
|
+
if (scope.kind === "member" || scope.kind === "service" || scope.kind === "elevated") return scope.ancestorOrgIds ?? [];
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Pure predicate: does this scope grant access to `targetOrgId`?
|
|
118
|
+
*
|
|
119
|
+
* Returns `true` if `targetOrgId` equals the scope's `organizationId` OR
|
|
120
|
+
* appears in `ancestorOrgIds`. Returns `false` otherwise — including for
|
|
121
|
+
* elevated scopes (this is a pure data query, not a permission check; the
|
|
122
|
+
* elevated bypass lives in `requireOrgInScope`, not here).
|
|
123
|
+
*
|
|
124
|
+
* Designed to be the building block for any custom hierarchy logic in your
|
|
125
|
+
* own permission checks. Use `requireOrgInScope` for the route-gating
|
|
126
|
+
* version that includes the elevated bypass.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* import { isOrgInScope } from '@classytic/arc/scope';
|
|
131
|
+
*
|
|
132
|
+
* // Inside a custom permission check
|
|
133
|
+
* if (!isOrgInScope(request.scope, request.params.orgId)) {
|
|
134
|
+
* return { granted: false, reason: 'Not in your org hierarchy' };
|
|
135
|
+
* }
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
function isOrgInScope(scope, targetOrgId) {
|
|
139
|
+
if (targetOrgId === void 0 || targetOrgId === null) return false;
|
|
140
|
+
if (scope.kind !== "member" && scope.kind !== "service" && scope.kind !== "elevated") return false;
|
|
141
|
+
if (scope.organizationId === targetOrgId) return true;
|
|
142
|
+
return (scope.ancestorOrgIds ?? []).includes(targetOrgId);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get userId from scope (available on authenticated, member, elevated).
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```typescript
|
|
149
|
+
* import { getUserId } from '@classytic/arc/scope';
|
|
150
|
+
* const userId = getUserId(request.scope);
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
function getUserId(scope) {
|
|
154
|
+
if (scope.kind === "public") return void 0;
|
|
155
|
+
return scope.userId;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get global user roles from scope (available on authenticated and member).
|
|
159
|
+
* These are user-level roles (e.g. superadmin, finance-admin) distinct from
|
|
160
|
+
* org-level roles (scope.orgRoles).
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* import { getUserRoles } from '@classytic/arc/scope';
|
|
165
|
+
* const globalRoles = getUserRoles(request.scope);
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
function getUserRoles(scope) {
|
|
169
|
+
if (scope.kind === "authenticated") return scope.userRoles ?? [];
|
|
170
|
+
if (scope.kind === "member") return scope.userRoles;
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Org context — canonical extraction from a Fastify request.
|
|
175
|
+
*
|
|
176
|
+
* Works regardless of auth type (JWT, Better Auth, custom) by reading
|
|
177
|
+
* `request.scope` and `request.user`. Eliminates the need for each resource
|
|
178
|
+
* to re-invent org extraction from headers/user/scope.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* import { getOrgContext } from '@classytic/arc/scope';
|
|
183
|
+
*
|
|
184
|
+
* handler: async (request, reply) => {
|
|
185
|
+
* const { userId, organizationId, roles, orgRoles } = getOrgContext(request);
|
|
186
|
+
* }
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
function getOrgContext(request) {
|
|
190
|
+
const scope = request.scope ?? { kind: "public" };
|
|
191
|
+
return {
|
|
192
|
+
userId: getUserId(scope) ?? request.user?.id ?? request.user?._id,
|
|
193
|
+
organizationId: getOrgId(scope) ?? request.user?.organizationId ?? request.headers?.["x-organization-id"],
|
|
194
|
+
roles: getUserRoles(scope),
|
|
195
|
+
orgRoles: getOrgRoles(scope)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Read `request.scope` safely from any object that *might* have one.
|
|
200
|
+
* Falls back to `PUBLIC_SCOPE` when the field is absent or undefined.
|
|
201
|
+
*
|
|
202
|
+
* This is the canonical way for permission checks, presets, and middleware
|
|
203
|
+
* to read scope — never access `request.scope` directly because it can be
|
|
204
|
+
* `undefined` on requests that haven't been touched by an auth adapter yet.
|
|
205
|
+
*
|
|
206
|
+
* Accepts a structural shape (`{ scope?: RequestScope }`) instead of the
|
|
207
|
+
* full Fastify request type so it can be called from any layer without
|
|
208
|
+
* dragging in the Fastify type. The actual runtime is identical.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```typescript
|
|
212
|
+
* import { getRequestScope } from '@classytic/arc/scope';
|
|
213
|
+
*
|
|
214
|
+
* function myCheck(ctx: PermissionContext) {
|
|
215
|
+
* const scope = getRequestScope(ctx.request);
|
|
216
|
+
* if (isElevated(scope)) return true;
|
|
217
|
+
* // ...
|
|
218
|
+
* }
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
function getRequestScope(request) {
|
|
222
|
+
return request.scope ?? PUBLIC_SCOPE;
|
|
223
|
+
}
|
|
224
|
+
/** Default public scope — used as initial decoration value */
|
|
225
|
+
const PUBLIC_SCOPE = Object.freeze({ kind: "public" });
|
|
226
|
+
/** Default authenticated scope — used when user is logged in but no org */
|
|
227
|
+
const AUTHENTICATED_SCOPE = Object.freeze({ kind: "authenticated" });
|
|
228
|
+
//#endregion
|
|
229
|
+
export { isElevated as _, getOrgContext as a, isService as b, getRequestScope as c, getServiceScopes as d, getTeamId as f, isAuthenticated as g, hasOrgAccess as h, getClientId as i, getScopeContext as l, getUserRoles as m, PUBLIC_SCOPE as n, getOrgId as o, getUserId as p, getAncestorOrgIds as r, getOrgRoles as s, AUTHENTICATED_SCOPE as t, getScopeContextMap as u, isMember as v, isOrgInScope as y };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { r as RequestScope } from "./types--D3vvfdt.mjs";
|
|
2
|
+
import { FastifyRequest } from "fastify";
|
|
3
|
+
|
|
4
|
+
//#region src/permissions/types.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* User base interface - minimal shape Arc expects
|
|
7
|
+
* Your actual User can have any additional fields
|
|
8
|
+
*/
|
|
9
|
+
interface UserBase {
|
|
10
|
+
id?: string;
|
|
11
|
+
_id?: string;
|
|
12
|
+
/** User roles — string (comma-separated), string[], or undefined. Matches Better Auth's admin plugin pattern. */
|
|
13
|
+
role?: string | string[];
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Extract normalized roles from a user object.
|
|
18
|
+
*
|
|
19
|
+
* Reads `user.role` which can be:
|
|
20
|
+
* - A comma-separated string: `"superadmin,user"` (Better Auth admin plugin)
|
|
21
|
+
* - A string array: `["admin", "user"]` (JWT / custom auth)
|
|
22
|
+
* - A single string: `"admin"`
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a raw role value (string, comma-separated string, or array) into a string[].
|
|
26
|
+
* Shared low-level helper used by both getUserRoles() and the Better Auth adapter.
|
|
27
|
+
*/
|
|
28
|
+
declare function normalizeRoles(value: unknown): string[];
|
|
29
|
+
declare function getUserRoles(user: UserBase | null | undefined): string[];
|
|
30
|
+
/**
|
|
31
|
+
* Context passed to permission check functions
|
|
32
|
+
*/
|
|
33
|
+
interface PermissionContext<TDoc = Record<string, unknown>> {
|
|
34
|
+
/** Authenticated user or null if unauthenticated */
|
|
35
|
+
user: UserBase | null;
|
|
36
|
+
/** Fastify request object */
|
|
37
|
+
request: FastifyRequest;
|
|
38
|
+
/** Resource name being accessed */
|
|
39
|
+
resource: string;
|
|
40
|
+
/** Action being performed (list, get, create, update, delete, or custom operation name) */
|
|
41
|
+
action: string;
|
|
42
|
+
/** Resource ID for single-resource operations (shortcut for params.id) */
|
|
43
|
+
resourceId?: string;
|
|
44
|
+
/** All route parameters (slug, parentId, custom params, etc.) */
|
|
45
|
+
params?: Record<string, string>;
|
|
46
|
+
/** Request body data */
|
|
47
|
+
data?: Partial<TDoc> | Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Result from a permission check.
|
|
51
|
+
*
|
|
52
|
+
* Permission checks can do three things:
|
|
53
|
+
* 1. **Grant or deny** access (`granted`, `reason`)
|
|
54
|
+
* 2. **Attach row-level filters** (`filters`) — these merge into `_policyFilters`
|
|
55
|
+
* and narrow subsequent queries (e.g. `{ userId: ctx.user.id }` for ownership)
|
|
56
|
+
* 3. **Install the request scope** (`scope`) — when a custom authenticator wants
|
|
57
|
+
* to set tenant/identity context directly from the permission layer, without
|
|
58
|
+
* relying on a separate auth plugin
|
|
59
|
+
*
|
|
60
|
+
* The `scope` field is the clean integration point for custom auth strategies
|
|
61
|
+
* (API keys, service accounts, gateway headers). When present, Arc writes it to
|
|
62
|
+
* `request.scope` which then flows through the normal tenant-filtering pipeline
|
|
63
|
+
* (QueryResolver + AccessControl). This is the idiomatic way to wire non-Better-Auth
|
|
64
|
+
* identity providers into Arc's multi-tenancy without touching the auth plugin layer.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* // Custom API-key auth — grant access AND install a service scope in one step
|
|
69
|
+
* export function requireApiKey(): PermissionCheck {
|
|
70
|
+
* return async ({ request }) => {
|
|
71
|
+
* const apiKey = request.headers['x-api-key'] as string | undefined;
|
|
72
|
+
* if (!apiKey) return { granted: false, reason: 'Missing API key' };
|
|
73
|
+
*
|
|
74
|
+
* const client = await ClientModel.findOne({ apiKey });
|
|
75
|
+
* if (!client) return { granted: false, reason: 'Invalid API key' };
|
|
76
|
+
*
|
|
77
|
+
* return {
|
|
78
|
+
* granted: true,
|
|
79
|
+
* // Install service scope — Arc writes this to request.scope automatically,
|
|
80
|
+
* // and tenantField filtering picks it up via metadata._scope
|
|
81
|
+
* scope: {
|
|
82
|
+
* kind: 'service',
|
|
83
|
+
* clientId: String(client._id),
|
|
84
|
+
* organizationId: String(client.companyId),
|
|
85
|
+
* scopes: client.allowedScopes,
|
|
86
|
+
* },
|
|
87
|
+
* // Optional row-level narrowing (e.g. per-project API keys)
|
|
88
|
+
* filters: client.projectId ? { projectId: client.projectId } : undefined,
|
|
89
|
+
* };
|
|
90
|
+
* };
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
interface PermissionResult {
|
|
95
|
+
/** Whether access is granted */
|
|
96
|
+
granted: boolean;
|
|
97
|
+
/** Reason for denial (for error messages) */
|
|
98
|
+
reason?: string;
|
|
99
|
+
/** Query filters to apply (for ownership / row-level security patterns) */
|
|
100
|
+
filters?: Record<string, unknown>;
|
|
101
|
+
/**
|
|
102
|
+
* Install this scope on `request.scope` when granted. Flows through to
|
|
103
|
+
* `metadata._scope` and is read by QueryResolver / AccessControl for
|
|
104
|
+
* tenant-field filtering. Use this to wire custom auth (API keys, service
|
|
105
|
+
* accounts, gateway headers) into Arc's multi-tenancy without a separate
|
|
106
|
+
* auth plugin.
|
|
107
|
+
*/
|
|
108
|
+
scope?: RequestScope;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Permission Check Function
|
|
112
|
+
*
|
|
113
|
+
* THE ONLY way to define permissions in Arc.
|
|
114
|
+
* Returns boolean, PermissionResult, or Promise of either.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* // Simple boolean return
|
|
119
|
+
* const isAdmin: PermissionCheck = (ctx) => getUserRoles(ctx.user).includes('admin');
|
|
120
|
+
*
|
|
121
|
+
* // With filters for ownership
|
|
122
|
+
* const ownedByUser: PermissionCheck = (ctx) => ({
|
|
123
|
+
* granted: true,
|
|
124
|
+
* filters: { userId: ctx.user?.id }
|
|
125
|
+
* });
|
|
126
|
+
*
|
|
127
|
+
* // Async check
|
|
128
|
+
* const canAccessOrg: PermissionCheck = async (ctx) => {
|
|
129
|
+
* const isMember = await checkMembership(ctx.user?.id, ctx.organizationId);
|
|
130
|
+
* return { granted: isMember, reason: isMember ? undefined : 'Not a member' };
|
|
131
|
+
* };
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
type PermissionCheck<TDoc = Record<string, unknown>> = ((context: PermissionContext<TDoc>) => boolean | PermissionResult | Promise<boolean | PermissionResult>) & PermissionCheckMeta;
|
|
135
|
+
/**
|
|
136
|
+
* Optional metadata attached to permission check functions.
|
|
137
|
+
* Used for OpenAPI docs, introspection, and route-level auth decisions.
|
|
138
|
+
*
|
|
139
|
+
* Each helper from `permissions/index.ts` writes its own discriminating tag
|
|
140
|
+
* so downstream tooling (OpenAPI generator, MCP resource builder, route
|
|
141
|
+
* audit utilities) can read off the requirement without re-parsing the
|
|
142
|
+
* function body. All fields are optional — only the helpers that emit them
|
|
143
|
+
* set them.
|
|
144
|
+
*/
|
|
145
|
+
interface PermissionCheckMeta {
|
|
146
|
+
/** Set by allowPublic() — marks the endpoint as publicly accessible */
|
|
147
|
+
_isPublic?: boolean;
|
|
148
|
+
/** Set by requireRoles() — the roles required for access */
|
|
149
|
+
_roles?: readonly string[];
|
|
150
|
+
/** Set by requireOrgMembership() — org-level permission type */
|
|
151
|
+
_orgPermission?: string;
|
|
152
|
+
/** Set by requireOrgRole() — the org roles required for access */
|
|
153
|
+
_orgRoles?: readonly string[];
|
|
154
|
+
/** Set by requireTeamMembership() — team-level permission type */
|
|
155
|
+
_teamPermission?: string;
|
|
156
|
+
/**
|
|
157
|
+
* Set by requireServiceScope() — the OAuth-style scope strings the
|
|
158
|
+
* caller's `service` identity must hold (any-match logic, parallels
|
|
159
|
+
* `_orgRoles`).
|
|
160
|
+
*/
|
|
161
|
+
_serviceScopes?: readonly string[];
|
|
162
|
+
/**
|
|
163
|
+
* Set by requireScopeContext() — the app-defined scope dimensions the
|
|
164
|
+
* caller must satisfy. Map keys are dimension names (`branchId`,
|
|
165
|
+
* `projectId`, etc.); values are the required string OR `undefined`
|
|
166
|
+
* for "must be present, any value".
|
|
167
|
+
*/
|
|
168
|
+
_scopeContext?: Record<string, string | undefined>;
|
|
169
|
+
/**
|
|
170
|
+
* Set by requireOrgInScope() — the target organization that must appear
|
|
171
|
+
* in the caller's org chain (current org or `ancestorOrgIds`). Either
|
|
172
|
+
* a static org id or a function extracting it from the request context
|
|
173
|
+
* (e.g. from route params).
|
|
174
|
+
*/
|
|
175
|
+
_orgInScopeTarget?: string | ((ctx: PermissionContext) => string | undefined);
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
export { getUserRoles as a, UserBase as i, PermissionContext as n, normalizeRoles as o, PermissionResult as r, PermissionCheck as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Vt as ResourceDefinition } from "./interface-
|
|
1
|
+
import { Vt as ResourceDefinition } from "./interface-B91alUzq.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
//#region src/integrations/mcp/types.d.ts
|
|
@@ -220,12 +220,28 @@ interface McpSession {
|
|
|
220
220
|
}
|
|
221
221
|
/** Resolved auth identity for a single MCP request */
|
|
222
222
|
interface McpAuthResult {
|
|
223
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Human user ID. Optional for service/machine principals — when `clientId`
|
|
225
|
+
* is set and `userId` is omitted, the principal is purely machine-identity
|
|
226
|
+
* and `ctx.user` will be `null` (not a synthetic user object).
|
|
227
|
+
*/
|
|
228
|
+
userId?: string;
|
|
224
229
|
organizationId?: string;
|
|
225
230
|
/** User roles (global) — used by guard helpers like requireRole() */
|
|
226
231
|
roles?: string[];
|
|
227
232
|
/** Org-level roles — used by guard helpers */
|
|
228
233
|
orgRoles?: string[];
|
|
234
|
+
/**
|
|
235
|
+
* OAuth client ID — set this to enable `kind: "service"` scope.
|
|
236
|
+
* When present, buildRequestContext produces a service scope instead
|
|
237
|
+
* of member/authenticated, enabling `requireServiceScope()` checks.
|
|
238
|
+
*/
|
|
239
|
+
clientId?: string;
|
|
240
|
+
/**
|
|
241
|
+
* OAuth scopes (e.g. `['read:products', 'write:orders']`).
|
|
242
|
+
* Carried on the `service` RequestScope for fine-grained permission checks.
|
|
243
|
+
*/
|
|
244
|
+
scopes?: readonly string[];
|
|
229
245
|
/** Any extra metadata from the auth resolver */
|
|
230
246
|
[key: string]: unknown;
|
|
231
247
|
}
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { G as OpenApiSchemas, Q as QueryParserInterface, q as ParsedQuery, u as AnyRecord } from "../interface-
|
|
2
|
-
import { a as NotFoundError, c as RateLimitError, d as ValidationError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-
|
|
3
|
-
import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-
|
|
1
|
+
import { G as OpenApiSchemas, Q as QueryParserInterface, q as ParsedQuery, u as AnyRecord } from "../interface-B91alUzq.mjs";
|
|
2
|
+
import { a as NotFoundError, c as RateLimitError, d as ValidationError, i as ForbiddenError, l as ServiceUnavailableError, m as isArcError, n as ConflictError, o as OrgAccessDeniedError, p as createError, r as ErrorDetails, s as OrgRequiredError, t as ArcError, u as UnauthorizedError } from "../errors-BS6lZvWy.mjs";
|
|
3
|
+
import { a as CircuitBreakerStats, c as createCircuitBreakerRegistry, i as CircuitBreakerRegistry, n as CircuitBreakerError, o as CircuitState, r as CircuitBreakerOptions, s as createCircuitBreaker, t as CircuitBreaker } from "../circuitBreaker-BBPDt-J_.mjs";
|
|
4
4
|
import { FastifyInstance } from "fastify";
|
|
5
5
|
|
|
6
6
|
//#region src/utils/compensation.d.ts
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as createQueryParser, t as ArcQueryParser } from "../queryParser-CgCtsjti.mjs";
|
|
2
|
-
import { a as createCircuitBreaker, i as CircuitState, n as CircuitBreakerError, o as createCircuitBreakerRegistry, r as CircuitBreakerRegistry, t as CircuitBreaker } from "../circuitBreaker-
|
|
3
|
-
import { _ as defineCompensation, a as getListQueryParams, c as listResponse, d as paginateWrapper, f as paginationSchema, g as wrapResponse, h as successResponseSchema, i as getDefaultCrudSchemas, l as messageWrapper, m as responses, n as deleteResponse, o as itemResponse, p as queryParams, r as errorResponseSchema, s as itemWrapper, t as createStateMachine, u as mutationResponse, v as withCompensation } from "../utils-
|
|
4
|
-
import { a as OrgAccessDeniedError, c as ServiceUnavailableError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-
|
|
5
|
-
import { a as toJsonSchema, i as isZodSchema, n as convertRouteSchema, r as isJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-
|
|
6
|
-
import { t as hasEvents } from "../typeGuards-
|
|
2
|
+
import { a as createCircuitBreaker, i as CircuitState, n as CircuitBreakerError, o as createCircuitBreakerRegistry, r as CircuitBreakerRegistry, t as CircuitBreaker } from "../circuitBreaker-l18oRgL5.mjs";
|
|
3
|
+
import { _ as defineCompensation, a as getListQueryParams, c as listResponse, d as paginateWrapper, f as paginationSchema, g as wrapResponse, h as successResponseSchema, i as getDefaultCrudSchemas, l as messageWrapper, m as responses, n as deleteResponse, o as itemResponse, p as queryParams, r as errorResponseSchema, s as itemWrapper, t as createStateMachine, u as mutationResponse, v as withCompensation } from "../utils-B-l6410F.mjs";
|
|
4
|
+
import { a as OrgAccessDeniedError, c as ServiceUnavailableError, f as createError, i as NotFoundError, l as UnauthorizedError, n as ConflictError, o as OrgRequiredError, p as isArcError, r as ForbiddenError, s as RateLimitError, t as ArcError, u as ValidationError } from "../errors-Cg58SLNi.mjs";
|
|
5
|
+
import { a as toJsonSchema, i as isZodSchema, n as convertRouteSchema, r as isJsonSchema, t as convertOpenApiSchemas } from "../schemaConverter-0TyONAwM.mjs";
|
|
6
|
+
import { t as hasEvents } from "../typeGuards-CcFZXgU7.mjs";
|
|
7
7
|
export { ArcError, ArcQueryParser, CircuitBreaker, CircuitBreakerError, CircuitBreakerRegistry, CircuitState, ConflictError, ForbiddenError, NotFoundError, OrgAccessDeniedError, OrgRequiredError, RateLimitError, ServiceUnavailableError, UnauthorizedError, ValidationError, convertOpenApiSchemas, convertRouteSchema, createCircuitBreaker, createCircuitBreakerRegistry, createError, createQueryParser, createStateMachine, defineCompensation, deleteResponse, errorResponseSchema, getDefaultCrudSchemas, getListQueryParams, hasEvents, isArcError, isJsonSchema, isZodSchema, itemResponse, itemWrapper, listResponse, messageWrapper, mutationResponse, paginateWrapper, paginationSchema, queryParams, responses, successResponseSchema, toJsonSchema, withCompensation, wrapResponse };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.3",
|
|
4
4
|
"description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -168,6 +168,10 @@
|
|
|
168
168
|
"types": "./dist/auth/redis-session.d.mts",
|
|
169
169
|
"default": "./dist/auth/redis-session.mjs"
|
|
170
170
|
},
|
|
171
|
+
"./auth/mongoose": {
|
|
172
|
+
"types": "./dist/auth/mongoose.d.mts",
|
|
173
|
+
"default": "./dist/auth/mongoose.mjs"
|
|
174
|
+
},
|
|
171
175
|
"./plugins/response-cache": {
|
|
172
176
|
"types": "./dist/plugins/response-cache.d.mts",
|
|
173
177
|
"default": "./dist/plugins/response-cache.mjs"
|
|
@@ -208,19 +212,22 @@
|
|
|
208
212
|
"lint:fix": "biome check --fix src/",
|
|
209
213
|
"lint:all": "biome check src/ tests/",
|
|
210
214
|
"test": "vitest run",
|
|
215
|
+
"test:main": "vitest run",
|
|
216
|
+
"test:perf": "node --expose-gc ./node_modules/vitest/vitest.mjs run --config vitest.perf.config.ts",
|
|
217
|
+
"test:ci": "npm run test:main && npm run test:perf",
|
|
211
218
|
"test:watch": "vitest",
|
|
212
219
|
"test:ui": "vitest --ui",
|
|
213
220
|
"test:coverage": "vitest run --coverage",
|
|
214
221
|
"test:e2e": "vitest run tests/e2e",
|
|
215
222
|
"test:unit": "vitest run tests/core tests/hooks tests/utils tests/plugins",
|
|
216
223
|
"smoke": "node scripts/smoke-test.mjs",
|
|
217
|
-
"prepublishOnly": "npm run typecheck && npm test && npm run build && npm run smoke"
|
|
224
|
+
"prepublishOnly": "npm run typecheck && npm run test:ci && npm run build && npm run smoke"
|
|
218
225
|
},
|
|
219
226
|
"engines": {
|
|
220
227
|
"node": ">=22"
|
|
221
228
|
},
|
|
222
229
|
"peerDependencies": {
|
|
223
|
-
"@classytic/mongokit": ">=3.5.
|
|
230
|
+
"@classytic/mongokit": ">=3.5.5",
|
|
224
231
|
"@classytic/streamline": ">=2.0.0",
|
|
225
232
|
"@fastify/cors": ">=11.0.0",
|
|
226
233
|
"@fastify/helmet": ">=13.0.0",
|
|
@@ -238,13 +245,13 @@
|
|
|
238
245
|
"@opentelemetry/instrumentation-mongodb": ">=0.40.0",
|
|
239
246
|
"@opentelemetry/sdk-node": ">=0.50.0",
|
|
240
247
|
"@sinclair/typebox": ">=0.34.0",
|
|
241
|
-
"better-auth": ">=1.
|
|
248
|
+
"better-auth": ">=1.6.0",
|
|
242
249
|
"bullmq": ">=5.0.0",
|
|
243
250
|
"fastify": ">=5.0.0",
|
|
244
251
|
"fastify-raw-body": ">=5.0.0",
|
|
245
252
|
"ioredis": ">=5.0.0",
|
|
246
253
|
"mongodb": ">=6.0.0",
|
|
247
|
-
"mongoose": ">=9.
|
|
254
|
+
"mongoose": ">=9.4.1",
|
|
248
255
|
"pino-pretty": ">=13.0.0",
|
|
249
256
|
"zod": ">=4.0.0"
|
|
250
257
|
},
|
|
@@ -337,22 +344,30 @@
|
|
|
337
344
|
"secure-json-parse": "^4.1.0"
|
|
338
345
|
},
|
|
339
346
|
"devDependencies": {
|
|
347
|
+
"@better-auth/mongo-adapter": "^1.6.0",
|
|
340
348
|
"@biomejs/biome": "^2.4.10",
|
|
341
|
-
"@classytic/mongokit": "^3.5.
|
|
349
|
+
"@classytic/mongokit": "^3.5.5",
|
|
350
|
+
"@fastify/cors": "^11.2.0",
|
|
351
|
+
"@fastify/helmet": "^13.0.2",
|
|
342
352
|
"@fastify/jwt": "^10.0.0",
|
|
343
353
|
"@fastify/multipart": "^9.0.0",
|
|
354
|
+
"@fastify/rate-limit": "^10.3.0",
|
|
355
|
+
"@fastify/sensible": "^6.0.4",
|
|
344
356
|
"@fastify/type-provider-typebox": "^6.0.0",
|
|
357
|
+
"@fastify/under-pressure": "^9.0.3",
|
|
345
358
|
"@fastify/websocket": "^11.0.0",
|
|
346
359
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
347
360
|
"@sinclair/typebox": "^0.34.0",
|
|
348
361
|
"@types/node": "^22.10.0",
|
|
349
362
|
"@types/qs": "^6.14.0",
|
|
350
363
|
"@vitest/coverage-v8": "^3.2.4",
|
|
364
|
+
"better-auth": "^1.6.0",
|
|
351
365
|
"fastify-raw-body": "^5.0.0",
|
|
352
366
|
"jsonwebtoken": "^9.0.0",
|
|
353
367
|
"knip": "^6.3.0",
|
|
354
368
|
"mongodb": "^7.1.0",
|
|
355
369
|
"mongodb-memory-server": "^11.0.1",
|
|
370
|
+
"mongoose": "^9.4.1",
|
|
356
371
|
"tsdown": "^0.21.7",
|
|
357
372
|
"typescript": "^6.0.2",
|
|
358
373
|
"vitest": "^3.0.0",
|