@classytic/arc 2.4.3 → 2.6.2
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 +57 -6
- package/dist/{BaseController-CkM5dUh_.mjs → BaseController-AbbRx3e0.mjs} +5 -2
- package/dist/{ResourceRegistry-DeCIFlix.mjs → ResourceRegistry-C6ngvOnn.mjs} +1 -0
- package/dist/adapters/index.d.mts +2 -2
- package/dist/adapters/index.mjs +1 -1
- package/dist/{adapters-DTC4Ug66.mjs → adapters-CTn28N4y.mjs} +72 -11
- package/dist/audit/index.d.mts +32 -6
- package/dist/audit/index.mjs +32 -4
- package/dist/audit/mongodb.d.mts +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +2 -2
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/init.mjs +12 -9
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +2 -2
- package/dist/{createApp-CBgVaFyh.mjs → createApp-D2w0LdYJ.mjs} +431 -290
- package/dist/{defineResource-B22gcNvn.mjs → defineResource-Ckxg6HrZ.mjs} +125 -22
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/dynamic/index.d.mts +1 -1
- package/dist/dynamic/index.mjs +2 -2
- package/dist/{elevation-Ca_yveIO.d.mts → elevation-C_taLQrM.d.mts} +27 -1
- package/dist/{errorHandler-DMbGdzBG.mjs → errorHandler-r2595m8T.mjs} +1 -1
- package/dist/{errors-CPpvPHT0.d.mts → errors-CcVbl1-T.d.mts} +17 -1
- package/dist/{errors-rxhfP7Hf.mjs → errors-NoQKsbAT.mjs} +23 -1
- package/dist/{eventPlugin-iGrSEmwJ.d.mts → eventPlugin-DW45v4V5.d.mts} +30 -2
- package/dist/events/index.d.mts +2 -2
- package/dist/events/index.mjs +40 -10
- package/dist/factory/index.d.mts +44 -23
- package/dist/factory/index.mjs +152 -2
- package/dist/hooks/index.d.mts +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-BL8CaQih.d.mts → index-B4uZm82R.d.mts} +2 -2
- package/dist/{index-yhxyjqNb.d.mts → index-DrCqa3Jq.d.mts} +4 -8
- package/dist/{index-Diqcm14c.d.mts → index-NGZksqM5.d.mts} +30 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +8 -7
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +4 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/{interface-DGmPxakH.d.mts → interface-CrN45qz1.d.mts} +229 -13
- package/dist/{mongodb-CUpYfxfD.d.mts → mongodb-kltrBPa1.d.mts} +10 -0
- package/dist/{mongodb-bga9AbkD.d.mts → mongodb-pMvOlR5_.d.mts} +1 -1
- package/dist/org/index.d.mts +1 -1
- package/dist/org/index.mjs +1 -1
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-Jk5x3sxz.mjs → permissions-C8ImI8gC.mjs} +44 -2
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +4 -4
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/index.d.mts +1 -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-OMPaHMTY.mjs → presets-BMfdy34e.mjs} +2 -2
- package/dist/{redis-CQ5YxMC5.d.mts → redis-D0Qc-9EW.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +1 -1
- package/dist/{resourceToTools-PMFE8HIv.mjs → resourceToTools-DH3c3e-T.mjs} +81 -7
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +2 -2
- package/dist/{sse-BkViJPlT.mjs → sse-BF7GR7IB.mjs} +1 -1
- package/dist/testing/index.d.mts +26 -3
- package/dist/testing/index.mjs +46 -2
- package/dist/types/index.d.mts +3 -3
- package/dist/types/index.mjs +23 -2
- package/dist/{types-C6TQjtdi.mjs → types-BhtYdxZU.mjs} +26 -1
- package/dist/{types-Dt0-AI6E.d.mts → types-C1Z28coa.d.mts} +195 -6
- package/dist/{types-BJmgxNbF.d.mts → types-DurlBP2N.d.mts} +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.mjs +1 -1
- package/package.json +6 -5
- package/skills/arc/SKILL.md +151 -4
- package/skills/arc/references/mcp.md +160 -2
- /package/dist/{interface-B4awm1RJ.d.mts → interface-gr-7qo9j.d.mts} +0 -0
package/README.md
CHANGED
|
@@ -15,26 +15,77 @@ npm install @classytic/mongokit mongoose # MongoDB adapter
|
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
17
|
import mongoose from 'mongoose';
|
|
18
|
-
import { createApp } from '@classytic/arc/factory';
|
|
18
|
+
import { createApp, loadResources } from '@classytic/arc/factory';
|
|
19
19
|
|
|
20
20
|
await mongoose.connect(process.env.DB_URI);
|
|
21
21
|
|
|
22
22
|
const app = await createApp({
|
|
23
23
|
preset: 'production',
|
|
24
|
+
resourcePrefix: '/api/v1',
|
|
25
|
+
resources: await loadResources(import.meta.url), // auto-discovers *.resource.ts
|
|
24
26
|
auth: { type: 'jwt', jwt: { secret: process.env.JWT_SECRET } },
|
|
25
27
|
cors: { origin: process.env.ALLOWED_ORIGINS?.split(',') },
|
|
26
28
|
});
|
|
27
29
|
|
|
28
|
-
await app.register(productResource.toPlugin());
|
|
29
30
|
await app.listen({ port: 8040, host: '0.0.0.0' });
|
|
30
31
|
```
|
|
31
32
|
|
|
33
|
+
Three ways to register resources:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Auto-discover from directory (recommended)
|
|
37
|
+
resources: await loadResources(import.meta.url), // dev/prod parity
|
|
38
|
+
|
|
39
|
+
// Explicit array
|
|
40
|
+
resources: [productResource, orderResource],
|
|
41
|
+
|
|
42
|
+
// Via plugins callback (full Fastify control)
|
|
43
|
+
plugins: async (f) => { await f.register(productResource.toPlugin()); },
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`loadResources()` discovers `default` exports, `export const resource`, OR any named export with `toPlugin()` (e.g. `export const userResource`). Per-resource opt-out of `resourcePrefix` via `skipGlobalPrefix: true` for webhooks/admin routes.
|
|
47
|
+
|
|
48
|
+
> **Import compatibility:** Works with relative imports and Node.js `#` subpath imports. Does **not** support tsconfig path aliases (`@/*`, `~/`) — use explicit `resources: [...]` instead.
|
|
49
|
+
|
|
50
|
+
## Boot Sequence
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
const app = await createApp({
|
|
54
|
+
resourcePrefix: '/api/v1',
|
|
55
|
+
plugins: async (f) => { await connectDB(); }, // 1. infra (DB, docs)
|
|
56
|
+
bootstrap: [inventoryInit, accountingInit], // 2. domain init
|
|
57
|
+
resources: await loadResources(import.meta.url), // 3. routes
|
|
58
|
+
afterResources: async (f) => { subscribeEvents(f); }, // 4. post-wiring
|
|
59
|
+
onReady: async (f) => { logger.info('ready'); },
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Audit (per-resource opt-in)
|
|
64
|
+
|
|
65
|
+
Clean DX without growing exclude lists:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// app.ts — register once with perResource mode
|
|
69
|
+
await fastify.register(auditPlugin, { autoAudit: { perResource: true } });
|
|
70
|
+
|
|
71
|
+
// order.resource.ts — opt in
|
|
72
|
+
defineResource({ name: 'order', audit: true });
|
|
73
|
+
|
|
74
|
+
// payment.resource.ts — only audit deletes
|
|
75
|
+
defineResource({ name: 'payment', audit: { operations: ['delete'] } });
|
|
76
|
+
|
|
77
|
+
// Manual logging from MCP tools or custom routes
|
|
78
|
+
app.post('/orders/:id/refund', async (req) => {
|
|
79
|
+
await app.audit.custom('order', req.params.id, 'refund', { reason }, { user });
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
32
83
|
## defineResource
|
|
33
84
|
|
|
34
85
|
Single API for a full REST resource with routes, permissions, and behaviors:
|
|
35
86
|
|
|
36
87
|
```typescript
|
|
37
|
-
import { defineResource, createMongooseAdapter, allowPublic,
|
|
88
|
+
import { defineResource, createMongooseAdapter, allowPublic, roles } from '@classytic/arc';
|
|
38
89
|
|
|
39
90
|
const productResource = defineResource({
|
|
40
91
|
name: 'product',
|
|
@@ -43,9 +94,9 @@ const productResource = defineResource({
|
|
|
43
94
|
permissions: {
|
|
44
95
|
list: allowPublic(),
|
|
45
96
|
get: allowPublic(),
|
|
46
|
-
create:
|
|
47
|
-
update:
|
|
48
|
-
delete:
|
|
97
|
+
create: roles('admin', 'editor'), // checks platform + org roles
|
|
98
|
+
update: roles('admin', 'editor'),
|
|
99
|
+
delete: roles('admin'),
|
|
49
100
|
},
|
|
50
101
|
cache: { staleTime: 30, gcTime: 300, tags: ['catalog'] }, // QueryCache (opt-in)
|
|
51
102
|
additionalRoutes: [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-Cxde4rpC.mjs";
|
|
2
|
-
import { d as
|
|
2
|
+
import { d as isElevated, f as isMember, i as getOrgId, n as PUBLIC_SCOPE } from "./types-BhtYdxZU.mjs";
|
|
3
3
|
import { t as buildQueryKey } from "./keys-qcD-TVJl.mjs";
|
|
4
4
|
import { getUserId } from "./types/index.mjs";
|
|
5
5
|
import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-ipsbIRPK.mjs";
|
|
@@ -247,7 +247,10 @@ var BodySanitizer = class {
|
|
|
247
247
|
let sanitized = { ...body };
|
|
248
248
|
for (const field of SYSTEM_FIELDS) delete sanitized[field];
|
|
249
249
|
const fieldRules = this.schemaOptions.fieldRules ?? {};
|
|
250
|
-
for (const [field, rules] of Object.entries(fieldRules))
|
|
250
|
+
for (const [field, rules] of Object.entries(fieldRules)) {
|
|
251
|
+
if (rules.systemManaged || rules.readonly) delete sanitized[field];
|
|
252
|
+
if (_operation === "update" && (rules.immutable || rules.immutableAfterCreate)) delete sanitized[field];
|
|
253
|
+
}
|
|
251
254
|
if (req) {
|
|
252
255
|
const arcContext = meta ?? req.metadata;
|
|
253
256
|
const scope = arcContext?._scope ?? PUBLIC_SCOPE;
|
|
@@ -51,6 +51,7 @@ var ResourceRegistry = class {
|
|
|
51
51
|
fieldPermissions: extractFieldPermissions(resource.fields),
|
|
52
52
|
pipelineSteps: extractPipelineSteps(resource.pipe),
|
|
53
53
|
rateLimit: resource.rateLimit,
|
|
54
|
+
audit: resource.audit,
|
|
54
55
|
plugin: resource.toPlugin()
|
|
55
56
|
};
|
|
56
57
|
this._resources.set(resource.name, entry);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as RepositoryLike, i as RelationMetadata, n as DataAdapter, o as SchemaMetadata, r as FieldMetadata, s as ValidationResult, t as AdapterFactory } from "../interface-
|
|
2
|
-
import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter } from "../index-
|
|
1
|
+
import { a as RepositoryLike, i as RelationMetadata, n as DataAdapter, o as SchemaMetadata, r as FieldMetadata, s as ValidationResult, t as AdapterFactory } from "../interface-CrN45qz1.mjs";
|
|
2
|
+
import { a as PrismaQueryParserOptions, c as MongooseAdapterOptions, i as PrismaQueryParser, l as createMongooseAdapter, n as PrismaAdapterOptions, o as createPrismaAdapter, r as PrismaQueryOptions, s as MongooseAdapter, t as PrismaAdapter } from "../index-DrCqa3Jq.mjs";
|
|
3
3
|
export { AdapterFactory, DataAdapter, FieldMetadata, MongooseAdapter, MongooseAdapterOptions, PrismaAdapter, PrismaAdapterOptions, PrismaQueryOptions, PrismaQueryParser, PrismaQueryParserOptions, RelationMetadata, RepositoryLike, SchemaMetadata, ValidationResult, createMongooseAdapter, createPrismaAdapter };
|
package/dist/adapters/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-
|
|
1
|
+
import { a as createMongooseAdapter, i as MongooseAdapter, n as PrismaQueryParser, r as createPrismaAdapter, t as PrismaAdapter } from "../adapters-CTn28N4y.mjs";
|
|
2
2
|
export { MongooseAdapter, PrismaAdapter, PrismaQueryParser, createMongooseAdapter, createPrismaAdapter };
|
|
@@ -78,17 +78,28 @@ var MongooseAdapter = class {
|
|
|
78
78
|
const properties = {};
|
|
79
79
|
const required = [];
|
|
80
80
|
const fieldRules = schemaOptions?.fieldRules || {};
|
|
81
|
-
const blockedFields = new Set(
|
|
81
|
+
const blockedFields = new Set([
|
|
82
|
+
...Object.entries(fieldRules).filter(([, rules]) => rules.systemManaged || rules.hidden).map(([field]) => field),
|
|
83
|
+
...schemaOptions?.excludeFields ?? [],
|
|
84
|
+
...schemaOptions?.hiddenFields ?? []
|
|
85
|
+
]);
|
|
86
|
+
const readonlySet = new Set(schemaOptions?.readonlyFields ?? []);
|
|
87
|
+
const optionalSet = new Set(schemaOptions?.optionalFields ?? []);
|
|
82
88
|
for (const [fieldName, schemaType] of Object.entries(paths)) {
|
|
83
89
|
if (fieldName.startsWith("__")) continue;
|
|
84
90
|
if (blockedFields.has(fieldName)) continue;
|
|
85
91
|
const typeInfo = schemaType;
|
|
86
92
|
properties[fieldName] = this.mongooseTypeToOpenApi(typeInfo);
|
|
87
|
-
if (typeInfo.isRequired) required.push(fieldName);
|
|
93
|
+
if (typeInfo.isRequired && !optionalSet.has(fieldName) && !fieldRules[fieldName]?.optional) required.push(fieldName);
|
|
88
94
|
}
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
95
|
+
const readonlyForInput = new Set([...readonlySet]);
|
|
96
|
+
for (const [field, rules] of Object.entries(fieldRules)) if (rules.immutable || rules.immutableAfterCreate) readonlyForInput.add(field);
|
|
97
|
+
const inputBlockedSet = new Set([...SYSTEM_FIELDS, ...readonlyForInput]);
|
|
98
|
+
const inputProperties = Object.fromEntries(Object.entries(properties).filter(([field]) => !inputBlockedSet.has(field)));
|
|
99
|
+
const inputRequired = required.filter((field) => !inputBlockedSet.has(field) && !blockedFields.has(field));
|
|
100
|
+
const immutableSet = /* @__PURE__ */ new Set();
|
|
101
|
+
for (const [field, rules] of Object.entries(fieldRules)) if (rules.immutable || rules.immutableAfterCreate) immutableSet.add(field);
|
|
102
|
+
const updateProperties = Object.fromEntries(Object.entries(inputProperties).filter(([field]) => !immutableSet.has(field)));
|
|
92
103
|
return {
|
|
93
104
|
createBody: {
|
|
94
105
|
type: "object",
|
|
@@ -97,11 +108,12 @@ var MongooseAdapter = class {
|
|
|
97
108
|
},
|
|
98
109
|
updateBody: {
|
|
99
110
|
type: "object",
|
|
100
|
-
properties:
|
|
111
|
+
properties: updateProperties
|
|
101
112
|
},
|
|
102
113
|
response: {
|
|
103
114
|
type: "object",
|
|
104
|
-
properties
|
|
115
|
+
properties,
|
|
116
|
+
additionalProperties: true
|
|
105
117
|
}
|
|
106
118
|
};
|
|
107
119
|
} catch {
|
|
@@ -147,18 +159,67 @@ var MongooseAdapter = class {
|
|
|
147
159
|
break;
|
|
148
160
|
case "Date":
|
|
149
161
|
baseType.type = "string";
|
|
150
|
-
baseType.format = "date-time";
|
|
151
162
|
break;
|
|
152
163
|
case "ObjectID":
|
|
153
164
|
case "ObjectId":
|
|
154
165
|
baseType.type = "string";
|
|
155
166
|
baseType.pattern = "^[a-f\\d]{24}$";
|
|
156
167
|
break;
|
|
157
|
-
case "Array":
|
|
168
|
+
case "Array": {
|
|
158
169
|
baseType.type = "array";
|
|
159
|
-
|
|
170
|
+
const ti = typeInfo;
|
|
171
|
+
if (ti.$isMongooseDocumentArray && ti.schema) {
|
|
172
|
+
const subSchema = ti.schema;
|
|
173
|
+
const subProps = {};
|
|
174
|
+
const subRequired = [];
|
|
175
|
+
for (const [subField, subType] of Object.entries(subSchema.paths)) {
|
|
176
|
+
if (subField.startsWith("_")) continue;
|
|
177
|
+
subProps[subField] = this.mongooseTypeToOpenApi(subType);
|
|
178
|
+
if (subType.isRequired) subRequired.push(subField);
|
|
179
|
+
}
|
|
180
|
+
baseType.items = {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: subProps,
|
|
183
|
+
...subRequired.length > 0 ? { required: subRequired } : {},
|
|
184
|
+
additionalProperties: true
|
|
185
|
+
};
|
|
186
|
+
} else if (ti.embeddedSchemaType?.instance) baseType.items = this.mongooseTypeToOpenApi(ti.embeddedSchemaType);
|
|
187
|
+
else baseType.items = {};
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case "Mixed":
|
|
191
|
+
baseType.type = [
|
|
192
|
+
"string",
|
|
193
|
+
"number",
|
|
194
|
+
"boolean",
|
|
195
|
+
"object",
|
|
196
|
+
"array"
|
|
197
|
+
];
|
|
198
|
+
break;
|
|
199
|
+
case "Map":
|
|
200
|
+
baseType.type = "object";
|
|
201
|
+
baseType.additionalProperties = true;
|
|
202
|
+
break;
|
|
203
|
+
case "Embedded":
|
|
204
|
+
case "SubDocument":
|
|
205
|
+
baseType.type = "object";
|
|
206
|
+
baseType.additionalProperties = true;
|
|
207
|
+
break;
|
|
208
|
+
case "Buffer":
|
|
209
|
+
baseType.type = "string";
|
|
210
|
+
baseType.format = "binary";
|
|
211
|
+
break;
|
|
212
|
+
case "Decimal128":
|
|
213
|
+
baseType.type = "string";
|
|
214
|
+
baseType.description = "Decimal128 (high-precision number as string)";
|
|
215
|
+
break;
|
|
216
|
+
case "UUID":
|
|
217
|
+
baseType.type = "string";
|
|
218
|
+
baseType.format = "uuid";
|
|
160
219
|
break;
|
|
161
|
-
default:
|
|
220
|
+
default:
|
|
221
|
+
baseType.type = "object";
|
|
222
|
+
baseType.additionalProperties = true;
|
|
162
223
|
}
|
|
163
224
|
return baseType;
|
|
164
225
|
}
|
package/dist/audit/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as AuditContext, c as AuditStore, i as AuditAction, l as AuditStoreOptions, n as MongoAuditStoreOptions, o as AuditEntry, r as MongoConnection, s as AuditQueryOptions, u as createAuditEntry } from "../mongodb-
|
|
1
|
+
import { a as AuditContext, c as AuditStore, i as AuditAction, l as AuditStoreOptions, n as MongoAuditStoreOptions, o as AuditEntry, r as MongoConnection, s as AuditQueryOptions, u as createAuditEntry } from "../mongodb-kltrBPa1.mjs";
|
|
2
2
|
import { FastifyPluginAsync } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/audit/auditPlugin.d.ts
|
|
@@ -19,14 +19,40 @@ interface AuditPluginOptions {
|
|
|
19
19
|
* Automatically audit CRUD operations via the hook system (default: true when enabled).
|
|
20
20
|
* When enabled, create/update/delete operations are auto-logged without manual calls.
|
|
21
21
|
*
|
|
22
|
-
* -
|
|
23
|
-
*
|
|
24
|
-
* -
|
|
25
|
-
*
|
|
22
|
+
* **Three opt-in patterns** — pick the one that matches your app:
|
|
23
|
+
*
|
|
24
|
+
* 1. **Per-resource opt-in (recommended for most apps)** — set `audit: true` on each
|
|
25
|
+
* resource. Audit only fires for those resources. No global `include`/`exclude` needed.
|
|
26
|
+
* ```ts
|
|
27
|
+
* defineResource({ name: 'order', audit: true });
|
|
28
|
+
* // auditPlugin auto-detects which resources opted in
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* 2. **Allowlist mode** — set `include: ['order', 'invoice']` for centralized config.
|
|
32
|
+
* Only listed resources are audited.
|
|
33
|
+
*
|
|
34
|
+
* 3. **Denylist mode** — set `exclude: ['health', 'metrics']` to audit everything except
|
|
35
|
+
* listed resources. Use sparingly — leads to growing exclude lists.
|
|
36
|
+
*
|
|
37
|
+
* Default behavior (`autoAudit: true`): denylist mode with no exclusions (audit everything).
|
|
38
|
+
* For most apps, switching to per-resource opt-in is cleaner.
|
|
39
|
+
*
|
|
40
|
+
* - `true`: Audit all CRUD operations on all resources (legacy default)
|
|
41
|
+
* - `{ operations: ['create', 'delete'] }`: Only specific operations
|
|
42
|
+
* - `{ include: ['order'] }`: Allowlist — only listed resources
|
|
43
|
+
* - `{ exclude: ['health'] }`: Denylist — all except listed
|
|
44
|
+
* - `{ perResource: true }`: Only resources with `audit: true` in their definition
|
|
45
|
+
* - `false`: Disable auto-audit (manual `fastify.audit.*()` calls only)
|
|
26
46
|
*/
|
|
27
47
|
autoAudit?: boolean | {
|
|
28
|
-
operations?: ("create" | "update" | "delete")[];
|
|
48
|
+
operations?: ("create" | "update" | "delete")[]; /** Allowlist — only listed resources are audited (mutually exclusive with exclude) */
|
|
49
|
+
include?: string[]; /** Denylist — audit everything except listed resources */
|
|
29
50
|
exclude?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Per-resource opt-in mode: only audit resources with `audit: true` in their
|
|
53
|
+
* `defineResource()` config. The cleanest pattern for most apps.
|
|
54
|
+
*/
|
|
55
|
+
perResource?: boolean;
|
|
30
56
|
};
|
|
31
57
|
}
|
|
32
58
|
declare module "fastify" {
|
package/dist/audit/index.mjs
CHANGED
|
@@ -11,7 +11,7 @@ function createAuditEntry(resource, documentId, action, context, data) {
|
|
|
11
11
|
resource,
|
|
12
12
|
documentId,
|
|
13
13
|
action,
|
|
14
|
-
userId: context.user
|
|
14
|
+
userId: extractUserId(context.user),
|
|
15
15
|
organizationId: context.organizationId,
|
|
16
16
|
before: data?.before,
|
|
17
17
|
after: data?.after,
|
|
@@ -24,6 +24,16 @@ function createAuditEntry(resource, documentId, action, context, data) {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
+
* Extract userId from a user object — DB-agnostic.
|
|
28
|
+
* Handles Mongoose ObjectId, string, number, or any type with toString().
|
|
29
|
+
*/
|
|
30
|
+
function extractUserId(user) {
|
|
31
|
+
if (!user) return void 0;
|
|
32
|
+
const raw = user._id ?? user.id;
|
|
33
|
+
if (raw == null) return void 0;
|
|
34
|
+
return typeof raw === "string" ? raw : String(raw);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
27
37
|
* Detect changed fields between two objects
|
|
28
38
|
*/
|
|
29
39
|
function detectChanges(before, after) {
|
|
@@ -170,16 +180,34 @@ const auditPlugin = async (fastify, opts = {}) => {
|
|
|
170
180
|
"update",
|
|
171
181
|
"delete"
|
|
172
182
|
];
|
|
173
|
-
const
|
|
174
|
-
const
|
|
183
|
+
const isObj = typeof autoAuditConfig === "object";
|
|
184
|
+
const ops = isObj ? autoAuditConfig.operations ?? defaultOps : defaultOps;
|
|
185
|
+
const includeResources = isObj && autoAuditConfig.include ? new Set(autoAuditConfig.include) : null;
|
|
186
|
+
const excludeResources = new Set(isObj ? autoAuditConfig.exclude ?? [] : []);
|
|
187
|
+
const perResourceMode = isObj ? autoAuditConfig.perResource === true : false;
|
|
188
|
+
if (includeResources && excludeResources.size > 0) fastify.log?.warn?.("Audit autoAudit: both 'include' and 'exclude' specified. Using 'include' (allowlist wins).");
|
|
175
189
|
fastify.addHook("onReady", async () => {
|
|
176
190
|
const arc = "arc" in fastify ? fastify.arc : void 0;
|
|
177
191
|
if (!arc?.hooks) {
|
|
178
192
|
fastify.log?.debug?.("Auto-audit skipped: arc-core plugin not registered");
|
|
179
193
|
return;
|
|
180
194
|
}
|
|
195
|
+
const optedInResources = /* @__PURE__ */ new Set();
|
|
196
|
+
const operationsByResource = /* @__PURE__ */ new Map();
|
|
197
|
+
if (perResourceMode && arc.registry) for (const entry of arc.registry.getAll()) {
|
|
198
|
+
const auditFlag = entry.audit;
|
|
199
|
+
if (!auditFlag) continue;
|
|
200
|
+
optedInResources.add(entry.name);
|
|
201
|
+
if (typeof auditFlag === "object" && auditFlag.operations) operationsByResource.set(entry.name, auditFlag.operations);
|
|
202
|
+
}
|
|
181
203
|
for (const op of ops) arc.hooks.after("*", op, async (ctx) => {
|
|
182
|
-
if (
|
|
204
|
+
if (perResourceMode) {
|
|
205
|
+
if (!optedInResources.has(ctx.resource)) return;
|
|
206
|
+
const allowedOps = operationsByResource.get(ctx.resource);
|
|
207
|
+
if (allowedOps && !allowedOps.includes(op)) return;
|
|
208
|
+
} else if (includeResources) {
|
|
209
|
+
if (!includeResources.has(ctx.resource)) return;
|
|
210
|
+
} else if (excludeResources.has(ctx.resource)) return;
|
|
183
211
|
const docId = autoAuditExtractId(ctx.result);
|
|
184
212
|
const scope = ctx.context?._scope;
|
|
185
213
|
const auditCtx = {
|
package/dist/audit/mongodb.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as MongoAuditStoreOptions, t as MongoAuditStore } from "../mongodb-
|
|
1
|
+
import { n as MongoAuditStoreOptions, t as MongoAuditStore } from "../mongodb-kltrBPa1.mjs";
|
|
2
2
|
export { MongoAuditStore, type MongoAuditStoreOptions };
|
package/dist/auth/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { h as AuthPluginOptions, m as AuthHelpers } from "../interface-CrN45qz1.mjs";
|
|
2
2
|
import { t as PermissionCheck } from "../types-BNUccdcf.mjs";
|
|
3
3
|
import { t as ExternalOpenApiPaths } from "../externalPaths-DpO-s7r8.mjs";
|
|
4
4
|
import { a as SessionManagerOptions, c as createSessionManager, i as SessionData, n as MemorySessionStoreOptions, o as SessionManagerResult, r as SessionCookieOptions, s as SessionStore, t as MemorySessionStore } from "../sessionManager-wbkYj2HL.mjs";
|
package/dist/auth/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as normalizeRoles, t as getUserRoles } from "../types-ZUu_h0jp.mjs";
|
|
2
|
-
import { t as ArcError } from "../errors-
|
|
3
|
-
import { c as requireOrgMembership, f as requireTeamMembership, l as requireOrgRole } from "../permissions-
|
|
2
|
+
import { t as ArcError } from "../errors-NoQKsbAT.mjs";
|
|
3
|
+
import { c as requireOrgMembership, f as requireTeamMembership, l as requireOrgRole } from "../permissions-C8ImI8gC.mjs";
|
|
4
4
|
import { n as extractBetterAuthOpenApi } from "../betterAuthOpenApi-lz0IRbXJ.mjs";
|
|
5
5
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
6
|
import fp from "fastify-plugin";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as ResourceRegistry } from "../../ResourceRegistry-
|
|
1
|
+
import { t as ResourceRegistry } from "../../ResourceRegistry-C6ngvOnn.mjs";
|
|
2
2
|
import { t as buildOpenApiSpec } from "../../openapi-CBmZ6EQN.mjs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import { accessSync } from "node:fs";
|
|
3
4
|
import { execSync, spawn } from "node:child_process";
|
|
4
|
-
import * as fs from "node:fs/promises";
|
|
5
5
|
import * as readline from "node:readline";
|
|
6
6
|
//#region src/cli/commands/init.ts
|
|
7
7
|
/**
|
|
@@ -694,13 +694,13 @@ import { getAuth } from './auth.js';
|
|
|
694
694
|
*/
|
|
695
695
|
|
|
696
696
|
${typeImport}import config from '#config/index.js';
|
|
697
|
-
import { createApp } from '@classytic/arc/factory';
|
|
697
|
+
import { createApp, loadResources } from '@classytic/arc/factory';
|
|
698
698
|
${betterAuthImport}
|
|
699
699
|
// App-specific plugins
|
|
700
700
|
import { registerPlugins } from '#plugins/index.js';
|
|
701
701
|
|
|
702
702
|
// Resource registry
|
|
703
|
-
import { registerResources } from '#resources/index.js';
|
|
703
|
+
import { resources, registerResources } from '#resources/index.js';
|
|
704
704
|
|
|
705
705
|
/**
|
|
706
706
|
* Create a fully configured app instance
|
|
@@ -708,9 +708,10 @@ import { registerResources } from '#resources/index.js';
|
|
|
708
708
|
* @returns Configured Fastify instance ready to use
|
|
709
709
|
*/
|
|
710
710
|
export async function createAppInstance()${ts ? ": Promise<FastifyInstance>" : ""} {
|
|
711
|
-
// Create Arc app with base configuration
|
|
711
|
+
// Create Arc app with resources and base configuration
|
|
712
712
|
const app = await createApp({
|
|
713
713
|
preset: config.env === 'production' ? (${config.edge ? "'edge'" : "'production'"}) : 'development',
|
|
714
|
+
resources,
|
|
714
715
|
${authConfig}
|
|
715
716
|
cors: {
|
|
716
717
|
origin: config.cors.origins,
|
|
@@ -727,9 +728,6 @@ export async function createAppInstance()${ts ? ": Promise<FastifyInstance>" : "
|
|
|
727
728
|
// Register app-specific plugins (explicit dependency injection)
|
|
728
729
|
await registerPlugins(app, { config });
|
|
729
730
|
|
|
730
|
-
// Register all resources
|
|
731
|
-
await registerResources(app);
|
|
732
|
-
|
|
733
731
|
return app;
|
|
734
732
|
}
|
|
735
733
|
|
|
@@ -1275,6 +1273,7 @@ import {
|
|
|
1275
1273
|
requireRoles,
|
|
1276
1274
|
requireOwnership,
|
|
1277
1275
|
allowPublic,
|
|
1276
|
+
roles,
|
|
1278
1277
|
anyOf,
|
|
1279
1278
|
allOf,
|
|
1280
1279
|
denyAll,
|
|
@@ -1287,6 +1286,7 @@ export {
|
|
|
1287
1286
|
requireAuth,
|
|
1288
1287
|
requireRoles,
|
|
1289
1288
|
requireOwnership,
|
|
1289
|
+
roles,
|
|
1290
1290
|
allOf,
|
|
1291
1291
|
anyOf,
|
|
1292
1292
|
denyAll,
|
|
@@ -1323,11 +1323,14 @@ export const requireSuperadmin = ()${returnType} =>
|
|
|
1323
1323
|
/**
|
|
1324
1324
|
* Organization-level guards (per-org member.role):
|
|
1325
1325
|
*
|
|
1326
|
-
* -
|
|
1326
|
+
* - roles('admin') — checks BOTH user.role AND org member.role (recommended)
|
|
1327
|
+
* - requireOrgRole(['admin','owner']) — checks member.role in active org ONLY
|
|
1327
1328
|
* - requireOrgMembership() — just checks if user is in the org (any role)
|
|
1328
1329
|
* - requireTeamMembership() — checks if user is in the active team
|
|
1329
1330
|
*
|
|
1330
|
-
*
|
|
1331
|
+
* RECOMMENDED: Use roles() for most cases — it checks platform + org roles automatically.
|
|
1332
|
+
* Use requireOrgRole() when you ONLY want org-level checks (exclude platform admins).
|
|
1333
|
+
*
|
|
1331
1334
|
* Platform superadmin automatically bypasses all org role checks.
|
|
1332
1335
|
*
|
|
1333
1336
|
* IMPORTANT: When using Better Auth's Access Control (ac) with custom roles,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as ResourceRegistry } from "../../ResourceRegistry-
|
|
1
|
+
import { t as ResourceRegistry } from "../../ResourceRegistry-C6ngvOnn.mjs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
//#region src/cli/commands/introspect.ts
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A as RESERVED_QUERY_PARAMS, C as HookOperation, D as MAX_SEARCH_LENGTH, E as MAX_REGEX_LENGTH, O as MUTATION_OPERATIONS, S as HOOK_PHASES, T as MAX_FILTER_DEPTH, _ as DEFAULT_MAX_LIMIT, a as getControllerScope, b as DEFAULT_UPDATE_METHOD, c as createPermissionMiddleware, d as IdempotencyService, f as createActionRouter, g as DEFAULT_LIMIT, h as DEFAULT_ID_FIELD, i as getControllerContext, j as SYSTEM_FIELDS, k as MutationOperation, l as ActionHandler, m as CrudOperation, n as createFastifyHandler, o as sendControllerResponse, p as CRUD_OPERATIONS, r as createRequestContext, s as createCrudRouter, t as createCrudHandlers, u as ActionRouterConfig, v as DEFAULT_SORT, w as HookPhase, x as HOOK_OPERATIONS, y as DEFAULT_TENANT_FIELD } from "../index-
|
|
1
|
+
import { At as AccessControlConfig, Bt as ResourceDefinition, Ct as BaseController, Dt as BodySanitizer, Et as QueryResolverConfig, Ot as BodySanitizerConfig, Tt as QueryResolver, Vt as defineResource, kt as AccessControl, wt as BaseControllerOptions } from "../interface-CrN45qz1.mjs";
|
|
2
|
+
import { A as RESERVED_QUERY_PARAMS, C as HookOperation, D as MAX_SEARCH_LENGTH, E as MAX_REGEX_LENGTH, O as MUTATION_OPERATIONS, S as HOOK_PHASES, T as MAX_FILTER_DEPTH, _ as DEFAULT_MAX_LIMIT, a as getControllerScope, b as DEFAULT_UPDATE_METHOD, c as createPermissionMiddleware, d as IdempotencyService, f as createActionRouter, g as DEFAULT_LIMIT, h as DEFAULT_ID_FIELD, i as getControllerContext, j as SYSTEM_FIELDS, k as MutationOperation, l as ActionHandler, m as CrudOperation, n as createFastifyHandler, o as sendControllerResponse, p as CRUD_OPERATIONS, r as createRequestContext, s as createCrudRouter, t as createCrudHandlers, u as ActionRouterConfig, v as DEFAULT_SORT, w as HookPhase, x as HOOK_OPERATIONS, y as DEFAULT_TENANT_FIELD } from "../index-B4uZm82R.mjs";
|
|
3
3
|
export { AccessControl, AccessControlConfig, ActionHandler, ActionRouterConfig, BaseController, BaseControllerOptions, BodySanitizer, BodySanitizerConfig, CRUD_OPERATIONS, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, IdempotencyService, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, createActionRouter, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineResource, getControllerContext, getControllerScope, sendControllerResponse };
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "../constants-Cxde4rpC.mjs";
|
|
2
|
-
import { i as AccessControl, n as QueryResolver, r as BodySanitizer, t as BaseController } from "../BaseController-
|
|
2
|
+
import { i as AccessControl, n as QueryResolver, r as BodySanitizer, t as BaseController } from "../BaseController-AbbRx3e0.mjs";
|
|
3
3
|
import { t as createActionRouter } from "../core-C1XCMtqM.mjs";
|
|
4
|
-
import { c as createCrudHandlers, d as getControllerContext, f as getControllerScope, l as createFastifyHandler, n as defineResource, o as createCrudRouter, p as sendControllerResponse, s as createPermissionMiddleware, t as ResourceDefinition, u as createRequestContext } from "../defineResource-
|
|
4
|
+
import { c as createCrudHandlers, d as getControllerContext, f as getControllerScope, l as createFastifyHandler, n as defineResource, o as createCrudRouter, p as sendControllerResponse, s as createPermissionMiddleware, t as ResourceDefinition, u as createRequestContext } from "../defineResource-Ckxg6HrZ.mjs";
|
|
5
5
|
export { AccessControl, BaseController, BodySanitizer, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, QueryResolver, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, createActionRouter, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineResource, getControllerContext, getControllerScope, sendControllerResponse };
|