@classytic/arc 2.6.1 → 2.6.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 +48 -2
- package/dist/{BaseController-AbbRx3e0.mjs → BaseController-DzRtluEF.mjs} +88 -8
- 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-CTn28N4y.mjs → adapters-gM-WYjNe.mjs} +6 -4
- package/dist/audit/index.d.mts +31 -5
- package/dist/audit/index.mjs +21 -3
- package/dist/auth/index.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- 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-Bol7DLUf.mjs → createApp-D2w0LdYJ.mjs} +27 -11
- package/dist/{defineResource-bVKHjQzE.mjs → defineResource-wWMBB4GP.mjs} +48 -30
- package/dist/docs/index.d.mts +1 -1
- package/dist/dynamic/index.d.mts +1 -1
- package/dist/dynamic/index.mjs +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +31 -15
- package/dist/hooks/index.d.mts +1 -1
- package/dist/{index-BIsZ_su5.d.mts → index-CHeJa4Zd.d.mts} +3 -3
- package/dist/{index-Cb3gtbg7.d.mts → index-gz6iuzCp.d.mts} +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +4 -4
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -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-DDW43OmS.d.mts → interface-DYH8AXGe.d.mts} +89 -4
- package/dist/org/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +1 -1
- package/dist/plugins/index.mjs +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +1 -1
- package/dist/{resourceToTools-DH3c3e-T.mjs → resourceToTools-nCJWnG1r.mjs} +250 -13
- package/dist/testing/index.d.mts +26 -3
- package/dist/testing/index.mjs +46 -2
- package/dist/types/index.d.mts +1 -1
- package/dist/{types-D5rjsS_i.d.mts → types-B4_TDdPe.d.mts} +1 -1
- package/dist/{types-D5hJ-k_3.d.mts → types-By-5mIfn.d.mts} +7 -1
- package/dist/utils/index.d.mts +1 -1
- package/package.json +18 -18
- package/skills/arc/SKILL.md +80 -8
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Three ways to register resources:
|
|
|
34
34
|
|
|
35
35
|
```typescript
|
|
36
36
|
// Auto-discover from directory (recommended)
|
|
37
|
-
resources: await loadResources(
|
|
37
|
+
resources: await loadResources(import.meta.url), // dev/prod parity
|
|
38
38
|
|
|
39
39
|
// Explicit array
|
|
40
40
|
resources: [productResource, orderResource],
|
|
@@ -43,7 +43,42 @@ resources: [productResource, orderResource],
|
|
|
43
43
|
plugins: async (f) => { await f.register(productResource.toPlugin()); },
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
|
|
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
|
+
```
|
|
47
82
|
|
|
48
83
|
## defineResource
|
|
49
84
|
|
|
@@ -73,6 +108,17 @@ const productResource = defineResource({
|
|
|
73
108
|
// Plus preset routes: GET /deleted, POST /:id/restore, GET /slug/:slug
|
|
74
109
|
```
|
|
75
110
|
|
|
111
|
+
**Custom primary key?** Use `idField` for resources keyed by UUIDs, slugs, or business identifiers:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
defineResource({
|
|
115
|
+
name: 'job',
|
|
116
|
+
adapter: createMongooseAdapter(JobModel, jobRepository),
|
|
117
|
+
idField: 'jobId', // routes + BaseController lookups + OpenAPI + MCP tools all use this
|
|
118
|
+
});
|
|
119
|
+
// GET /jobs/job-5219f346-a4d → 200 (no ObjectId pattern enforcement)
|
|
120
|
+
```
|
|
121
|
+
|
|
76
122
|
## Authentication
|
|
77
123
|
|
|
78
124
|
Auth uses a discriminated union — pick a `type`:
|
|
@@ -96,9 +96,12 @@ var AccessControl = class AccessControl {
|
|
|
96
96
|
*/
|
|
97
97
|
async fetchWithAccessControl(id, req, repository, queryOptions) {
|
|
98
98
|
const compoundFilter = this.buildIdFilter(id, req);
|
|
99
|
-
const
|
|
99
|
+
const needsCompoundLookup = Object.keys(compoundFilter).length > 1 || this.idField !== "_id";
|
|
100
100
|
try {
|
|
101
|
-
if (
|
|
101
|
+
if (needsCompoundLookup && typeof repository.getOne === "function") return await repository.getOne(compoundFilter, queryOptions);
|
|
102
|
+
if (this.idField !== "_id") {
|
|
103
|
+
if (typeof repository.getOne !== "function") throw new Error(`Resource with idField="${this.idField}" requires repository.getOne() to look up by custom field. Arc's BaseController cannot fall back to getById() because it would query by _id.`);
|
|
104
|
+
}
|
|
102
105
|
const item = await repository.getById(id, queryOptions);
|
|
103
106
|
if (!item) return null;
|
|
104
107
|
const arcContext = this._meta(req);
|
|
@@ -719,6 +722,7 @@ var BaseController = class {
|
|
|
719
722
|
details: { code: "OWNERSHIP_DENIED" },
|
|
720
723
|
status: 403
|
|
721
724
|
};
|
|
725
|
+
const repoId = this.idField !== "_id" && existing ? String(existing["_id"] ?? id) : id;
|
|
722
726
|
const hooks = this.getHooks(req);
|
|
723
727
|
let processedData = data;
|
|
724
728
|
if (hooks && this.resourceName) try {
|
|
@@ -741,7 +745,7 @@ var BaseController = class {
|
|
|
741
745
|
status: 400
|
|
742
746
|
};
|
|
743
747
|
}
|
|
744
|
-
const repoUpdate = async () => this.repository.update(
|
|
748
|
+
const repoUpdate = async () => this.repository.update(repoId, processedData, {
|
|
745
749
|
user,
|
|
746
750
|
context: arcContext
|
|
747
751
|
});
|
|
@@ -797,6 +801,7 @@ var BaseController = class {
|
|
|
797
801
|
details: { code: "OWNERSHIP_DENIED" },
|
|
798
802
|
status: 403
|
|
799
803
|
};
|
|
804
|
+
const repoId = this.idField !== "_id" && existing ? String(existing["_id"] ?? id) : id;
|
|
800
805
|
const hooks = this.getHooks(req);
|
|
801
806
|
if (hooks && this.resourceName) try {
|
|
802
807
|
await hooks.executeBefore(this.resourceName, "delete", existing, {
|
|
@@ -815,7 +820,7 @@ var BaseController = class {
|
|
|
815
820
|
status: 400
|
|
816
821
|
};
|
|
817
822
|
}
|
|
818
|
-
const repoDelete = async () => this.repository.delete(
|
|
823
|
+
const repoDelete = async () => this.repository.delete(repoId, {
|
|
819
824
|
user,
|
|
820
825
|
context: arcContext
|
|
821
826
|
});
|
|
@@ -922,7 +927,8 @@ var BaseController = class {
|
|
|
922
927
|
details: { code: "OWNERSHIP_DENIED" },
|
|
923
928
|
status: 403
|
|
924
929
|
};
|
|
925
|
-
const
|
|
930
|
+
const repoId = this.idField !== "_id" && existing ? String(existing["_id"] ?? id) : id;
|
|
931
|
+
const item = await repo.restore(repoId);
|
|
926
932
|
if (!item) return {
|
|
927
933
|
success: false,
|
|
928
934
|
error: "Resource not found",
|
|
@@ -978,7 +984,33 @@ var BaseController = class {
|
|
|
978
984
|
error: "Bulk create requires a non-empty items array",
|
|
979
985
|
status: 400
|
|
980
986
|
};
|
|
981
|
-
|
|
987
|
+
let scopedItems = items;
|
|
988
|
+
if (this.tenantField) {
|
|
989
|
+
const scope = this.meta(req)?._scope;
|
|
990
|
+
if (scope) {
|
|
991
|
+
if (scope.kind === "public") return {
|
|
992
|
+
success: false,
|
|
993
|
+
error: "Organization context required to bulk-create resources",
|
|
994
|
+
details: { code: "ORG_CONTEXT_REQUIRED" },
|
|
995
|
+
status: 403
|
|
996
|
+
};
|
|
997
|
+
if (!isElevated(scope)) {
|
|
998
|
+
const orgId = getOrgId(scope);
|
|
999
|
+
if (!orgId) return {
|
|
1000
|
+
success: false,
|
|
1001
|
+
error: "Organization context required to bulk-create resources",
|
|
1002
|
+
details: { code: "ORG_CONTEXT_REQUIRED" },
|
|
1003
|
+
status: 403
|
|
1004
|
+
};
|
|
1005
|
+
const tenantField = this.tenantField;
|
|
1006
|
+
scopedItems = items.map((item) => ({
|
|
1007
|
+
...item,
|
|
1008
|
+
[tenantField]: orgId
|
|
1009
|
+
}));
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
const created = await repo.createMany(scopedItems);
|
|
982
1014
|
return {
|
|
983
1015
|
success: true,
|
|
984
1016
|
data: created,
|
|
@@ -986,6 +1018,40 @@ var BaseController = class {
|
|
|
986
1018
|
meta: { count: created.length }
|
|
987
1019
|
};
|
|
988
1020
|
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Build a tenant-scoped filter for bulk update/delete.
|
|
1023
|
+
*
|
|
1024
|
+
* Mirrors `AccessControl.buildIdFilter` semantics for single-doc ops:
|
|
1025
|
+
* - Always merge `_policyFilters` (from permission middleware)
|
|
1026
|
+
* - When `tenantField` is set AND a `member` scope is present, add the
|
|
1027
|
+
* org filter so cross-tenant data can't be touched.
|
|
1028
|
+
* - When the scope is `elevated` (platform admin), no org filter is
|
|
1029
|
+
* applied — admins can bulk-update across orgs intentionally.
|
|
1030
|
+
* - When the scope is `public` on a tenant-scoped resource, deny.
|
|
1031
|
+
* - When NO scope is present at all (e.g., direct controller calls in
|
|
1032
|
+
* unit tests, or app routes without auth middleware), the controller
|
|
1033
|
+
* stays lenient — it's the middleware layer's job to fail-close.
|
|
1034
|
+
* Apps that want fail-close on bulk routes should run the multi-tenant
|
|
1035
|
+
* preset middleware (or equivalent) ahead of these handlers.
|
|
1036
|
+
*
|
|
1037
|
+
* Returns the merged filter, or `null` when access must be denied.
|
|
1038
|
+
*/
|
|
1039
|
+
buildBulkFilter(userFilter, req) {
|
|
1040
|
+
const filter = { ...userFilter };
|
|
1041
|
+
const arcContext = this.meta(req);
|
|
1042
|
+
const policyFilters = arcContext?._policyFilters;
|
|
1043
|
+
if (policyFilters) Object.assign(filter, policyFilters);
|
|
1044
|
+
if (this.tenantField) {
|
|
1045
|
+
const scope = arcContext?._scope;
|
|
1046
|
+
if (!scope) return filter;
|
|
1047
|
+
if (scope.kind === "public") return null;
|
|
1048
|
+
if (isElevated(scope)) return filter;
|
|
1049
|
+
const orgId = getOrgId(scope);
|
|
1050
|
+
if (!orgId) return null;
|
|
1051
|
+
filter[this.tenantField] = orgId;
|
|
1052
|
+
}
|
|
1053
|
+
return filter;
|
|
1054
|
+
}
|
|
989
1055
|
async bulkUpdate(req) {
|
|
990
1056
|
const repo = this.repository;
|
|
991
1057
|
if (!repo.updateMany) return {
|
|
@@ -1004,9 +1070,16 @@ var BaseController = class {
|
|
|
1004
1070
|
error: "Bulk update requires non-empty data",
|
|
1005
1071
|
status: 400
|
|
1006
1072
|
};
|
|
1073
|
+
const scopedFilter = this.buildBulkFilter(body.filter, req);
|
|
1074
|
+
if (scopedFilter === null) return {
|
|
1075
|
+
success: false,
|
|
1076
|
+
error: "Organization context required for bulk update",
|
|
1077
|
+
details: { code: "ORG_CONTEXT_REQUIRED" },
|
|
1078
|
+
status: 403
|
|
1079
|
+
};
|
|
1007
1080
|
return {
|
|
1008
1081
|
success: true,
|
|
1009
|
-
data: await repo.updateMany(
|
|
1082
|
+
data: await repo.updateMany(scopedFilter, body.data),
|
|
1010
1083
|
status: 200
|
|
1011
1084
|
};
|
|
1012
1085
|
}
|
|
@@ -1023,9 +1096,16 @@ var BaseController = class {
|
|
|
1023
1096
|
error: "Bulk delete requires a non-empty filter",
|
|
1024
1097
|
status: 400
|
|
1025
1098
|
};
|
|
1099
|
+
const scopedFilter = this.buildBulkFilter(body.filter, req);
|
|
1100
|
+
if (scopedFilter === null) return {
|
|
1101
|
+
success: false,
|
|
1102
|
+
error: "Organization context required for bulk delete",
|
|
1103
|
+
details: { code: "ORG_CONTEXT_REQUIRED" },
|
|
1104
|
+
status: 403
|
|
1105
|
+
};
|
|
1026
1106
|
return {
|
|
1027
1107
|
success: true,
|
|
1028
|
-
data: await repo.deleteMany(
|
|
1108
|
+
data: await repo.deleteMany(scopedFilter),
|
|
1029
1109
|
status: 200
|
|
1030
1110
|
};
|
|
1031
1111
|
}
|
|
@@ -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
|
|
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 RelationMetadata, c as ValidationResult, i as FieldMetadata, o as RepositoryLike, r as DataAdapter, s as SchemaMetadata, t as AdapterFactory } from "../interface-DYH8AXGe.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-CHeJa4Zd.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-gM-WYjNe.mjs";
|
|
2
2
|
export { MongooseAdapter, PrismaAdapter, PrismaQueryParser, createMongooseAdapter, createPrismaAdapter };
|
|
@@ -71,9 +71,9 @@ var MongooseAdapter = class {
|
|
|
71
71
|
* If a `schemaGenerator` plugin was provided (e.g. MongoKit's buildCrudSchemasFromModel),
|
|
72
72
|
* it is used instead of the built-in basic conversion.
|
|
73
73
|
*/
|
|
74
|
-
generateSchemas(schemaOptions) {
|
|
74
|
+
generateSchemas(schemaOptions, context) {
|
|
75
75
|
try {
|
|
76
|
-
if (this.schemaGenerator) return this.schemaGenerator(this.model, schemaOptions);
|
|
76
|
+
if (this.schemaGenerator) return this.schemaGenerator(this.model, schemaOptions, context);
|
|
77
77
|
const paths = this.model.schema.paths;
|
|
78
78
|
const properties = {};
|
|
79
79
|
const required = [];
|
|
@@ -104,11 +104,13 @@ var MongooseAdapter = class {
|
|
|
104
104
|
createBody: {
|
|
105
105
|
type: "object",
|
|
106
106
|
properties: inputProperties,
|
|
107
|
-
required: inputRequired.length > 0 ? inputRequired : void 0
|
|
107
|
+
required: inputRequired.length > 0 ? inputRequired : void 0,
|
|
108
|
+
additionalProperties: true
|
|
108
109
|
},
|
|
109
110
|
updateBody: {
|
|
110
111
|
type: "object",
|
|
111
|
-
properties: updateProperties
|
|
112
|
+
properties: updateProperties,
|
|
113
|
+
additionalProperties: true
|
|
112
114
|
},
|
|
113
115
|
response: {
|
|
114
116
|
type: "object",
|
package/dist/audit/index.d.mts
CHANGED
|
@@ -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
|
@@ -180,16 +180,34 @@ const auditPlugin = async (fastify, opts = {}) => {
|
|
|
180
180
|
"update",
|
|
181
181
|
"delete"
|
|
182
182
|
];
|
|
183
|
-
const
|
|
184
|
-
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).");
|
|
185
189
|
fastify.addHook("onReady", async () => {
|
|
186
190
|
const arc = "arc" in fastify ? fastify.arc : void 0;
|
|
187
191
|
if (!arc?.hooks) {
|
|
188
192
|
fastify.log?.debug?.("Auto-audit skipped: arc-core plugin not registered");
|
|
189
193
|
return;
|
|
190
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
|
+
}
|
|
191
203
|
for (const op of ops) arc.hooks.after("*", op, async (ctx) => {
|
|
192
|
-
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;
|
|
193
211
|
const docId = autoAuditExtractId(ctx.result);
|
|
194
212
|
const scope = ctx.context?._scope;
|
|
195
213
|
const auditCtx = {
|
package/dist/auth/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { g as AuthPluginOptions, h as AuthHelpers } from "../interface-DYH8AXGe.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";
|
|
@@ -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,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 { At as
|
|
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 AccessControl, Dt as QueryResolverConfig, Et as QueryResolver, Ht as defineResource, Ot as BodySanitizer, Tt as BaseControllerOptions, Vt as ResourceDefinition, jt as AccessControlConfig, kt as BodySanitizerConfig, wt as BaseController } from "../interface-DYH8AXGe.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-gz6iuzCp.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-DzRtluEF.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-wWMBB4GP.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 };
|
|
@@ -66,20 +66,36 @@ const productionPreset = {
|
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
68
|
/**
|
|
69
|
+
* Try to detect if `pino-pretty` is installed (devDep). Returns the transport
|
|
70
|
+
* config if available, or falls back to plain JSON logging. This prevents the
|
|
71
|
+
* common "pino-pretty not found" crash in production when someone uses the
|
|
72
|
+
* development preset by mistake (or via NODE_ENV-based preset selection).
|
|
73
|
+
*/
|
|
74
|
+
function devLoggerConfig() {
|
|
75
|
+
try {
|
|
76
|
+
const req = eval("require") ?? null;
|
|
77
|
+
if (req?.resolve) {
|
|
78
|
+
req.resolve("pino-pretty");
|
|
79
|
+
return {
|
|
80
|
+
level: "debug",
|
|
81
|
+
transport: {
|
|
82
|
+
target: "pino-pretty",
|
|
83
|
+
options: {
|
|
84
|
+
colorize: true,
|
|
85
|
+
translateTime: "SYS:HH:MM:ss",
|
|
86
|
+
ignore: "pid,hostname"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
} catch {}
|
|
92
|
+
return { level: "debug" };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
69
95
|
* Development preset - relaxed security, verbose logging
|
|
70
96
|
*/
|
|
71
97
|
const developmentPreset = {
|
|
72
|
-
logger:
|
|
73
|
-
level: "debug",
|
|
74
|
-
transport: {
|
|
75
|
-
target: "pino-pretty",
|
|
76
|
-
options: {
|
|
77
|
-
colorize: true,
|
|
78
|
-
translateTime: "SYS:HH:MM:ss",
|
|
79
|
-
ignore: "pid,hostname"
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
},
|
|
98
|
+
logger: devLoggerConfig(),
|
|
83
99
|
trustProxy: true,
|
|
84
100
|
helmet: { contentSecurityPolicy: false },
|
|
85
101
|
cors: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-Cxde4rpC.mjs";
|
|
2
2
|
import { d as isElevated, f as isMember, n as PUBLIC_SCOPE } from "./types-BhtYdxZU.mjs";
|
|
3
|
-
import { t as BaseController } from "./BaseController-
|
|
3
|
+
import { t as BaseController } from "./BaseController-DzRtluEF.mjs";
|
|
4
4
|
import { i as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-ipsbIRPK.mjs";
|
|
5
5
|
import { t as getUserRoles } from "./types-ZUu_h0jp.mjs";
|
|
6
6
|
import { t as requestContext } from "./requestContext-DYtmNpm5.mjs";
|
|
@@ -1013,19 +1013,52 @@ function defineResource(config) {
|
|
|
1013
1013
|
resource._pendingHooks.push(...inlineHooks);
|
|
1014
1014
|
}
|
|
1015
1015
|
if (!config.skipRegistry) try {
|
|
1016
|
-
let openApiSchemas
|
|
1017
|
-
if (
|
|
1018
|
-
const
|
|
1016
|
+
let openApiSchemas;
|
|
1017
|
+
if (config.adapter?.generateSchemas) {
|
|
1018
|
+
const adapterContext = {
|
|
1019
|
+
idField: config.idField,
|
|
1020
|
+
resourceName: config.name
|
|
1021
|
+
};
|
|
1022
|
+
const generated = config.adapter.generateSchemas(config.schemaOptions, adapterContext);
|
|
1019
1023
|
if (generated) openApiSchemas = generated;
|
|
1020
1024
|
}
|
|
1025
|
+
if (config.idField && config.idField !== "_id" && openApiSchemas?.params && typeof openApiSchemas.params === "object") {
|
|
1026
|
+
const params = openApiSchemas.params;
|
|
1027
|
+
const properties = params.properties;
|
|
1028
|
+
const idProp = properties?.id;
|
|
1029
|
+
if (idProp && typeof idProp === "object") {
|
|
1030
|
+
const pattern = idProp.pattern;
|
|
1031
|
+
if (typeof pattern === "string" && (pattern === "^[0-9a-fA-F]{24}$" || pattern === "^[a-f\\d]{24}$" || pattern === "^[a-fA-F0-9]{24}$" || /^\^\[[a-fA-F0-9\\d]+\]\{24\}\$$/.test(pattern))) {
|
|
1032
|
+
const cleanedId = { ...idProp };
|
|
1033
|
+
delete cleanedId.pattern;
|
|
1034
|
+
delete cleanedId.minLength;
|
|
1035
|
+
delete cleanedId.maxLength;
|
|
1036
|
+
if (!cleanedId.description) cleanedId.description = `${config.idField} (custom ID field)`;
|
|
1037
|
+
openApiSchemas = {
|
|
1038
|
+
...openApiSchemas,
|
|
1039
|
+
params: {
|
|
1040
|
+
...params,
|
|
1041
|
+
properties: {
|
|
1042
|
+
...properties,
|
|
1043
|
+
id: cleanedId
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1021
1050
|
const queryParser = config.queryParser;
|
|
1022
|
-
if (
|
|
1051
|
+
if (queryParser?.getQuerySchema) {
|
|
1023
1052
|
const querySchema = queryParser.getQuerySchema();
|
|
1024
1053
|
if (querySchema) openApiSchemas = {
|
|
1025
1054
|
...openApiSchemas,
|
|
1026
1055
|
listQuery: querySchema
|
|
1027
1056
|
};
|
|
1028
1057
|
}
|
|
1058
|
+
if (config.openApiSchemas) openApiSchemas = {
|
|
1059
|
+
...openApiSchemas,
|
|
1060
|
+
...config.openApiSchemas
|
|
1061
|
+
};
|
|
1029
1062
|
if (openApiSchemas) openApiSchemas = convertOpenApiSchemas(openApiSchemas);
|
|
1030
1063
|
resource._registryMeta = {
|
|
1031
1064
|
module: config.module,
|
|
@@ -1050,6 +1083,7 @@ var ResourceDefinition = class {
|
|
|
1050
1083
|
disabledRoutes;
|
|
1051
1084
|
events;
|
|
1052
1085
|
rateLimit;
|
|
1086
|
+
audit;
|
|
1053
1087
|
updateMethod;
|
|
1054
1088
|
pipe;
|
|
1055
1089
|
fields;
|
|
@@ -1078,6 +1112,7 @@ var ResourceDefinition = class {
|
|
|
1078
1112
|
this.disabledRoutes = config.disabledRoutes ?? [];
|
|
1079
1113
|
this.events = config.events ?? {};
|
|
1080
1114
|
this.rateLimit = config.rateLimit;
|
|
1115
|
+
this.audit = config.audit;
|
|
1081
1116
|
this.updateMethod = config.updateMethod;
|
|
1082
1117
|
this.pipe = config.pipe;
|
|
1083
1118
|
this.fields = config.fields;
|
|
@@ -1150,7 +1185,7 @@ var ResourceDefinition = class {
|
|
|
1150
1185
|
const openApi = self._registryMeta?.openApiSchemas;
|
|
1151
1186
|
if (openApi && (!self.customSchemas || Object.keys(self.customSchemas).length === 0)) {
|
|
1152
1187
|
const generated = {};
|
|
1153
|
-
const { createBody, updateBody, params
|
|
1188
|
+
const { createBody, updateBody, params } = openApi;
|
|
1154
1189
|
const safeBody = (schema) => {
|
|
1155
1190
|
if (schema && typeof schema === "object" && schema.type === "object") return {
|
|
1156
1191
|
additionalProperties: true,
|
|
@@ -1183,39 +1218,22 @@ var ResourceDefinition = class {
|
|
|
1183
1218
|
}
|
|
1184
1219
|
const listQuerySchema = self._registryMeta?.openApiSchemas?.listQuery;
|
|
1185
1220
|
if (listQuerySchema) {
|
|
1186
|
-
const
|
|
1221
|
+
const KEEP_AS_IS = new Set([
|
|
1187
1222
|
"page",
|
|
1188
1223
|
"limit",
|
|
1189
1224
|
"sort",
|
|
1190
1225
|
"search",
|
|
1191
1226
|
"select",
|
|
1192
|
-
"after"
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
"
|
|
1196
|
-
"minimum",
|
|
1197
|
-
"maximum",
|
|
1198
|
-
"minLength",
|
|
1199
|
-
"maxLength",
|
|
1200
|
-
"pattern",
|
|
1201
|
-
"format",
|
|
1202
|
-
"exclusiveMinimum",
|
|
1203
|
-
"exclusiveMaximum",
|
|
1204
|
-
"multipleOf",
|
|
1205
|
-
"minItems",
|
|
1206
|
-
"maxItems",
|
|
1207
|
-
"uniqueItems"
|
|
1227
|
+
"after",
|
|
1228
|
+
"populate",
|
|
1229
|
+
"lookup",
|
|
1230
|
+
"aggregate"
|
|
1208
1231
|
]);
|
|
1209
1232
|
const props = listQuerySchema.properties;
|
|
1210
1233
|
const normalizedProps = props ? { ...props } : void 0;
|
|
1211
1234
|
if (normalizedProps) for (const key of Object.keys(normalizedProps)) {
|
|
1212
|
-
if (
|
|
1213
|
-
|
|
1214
|
-
if (prop && typeof prop === "object" && "type" in prop) {
|
|
1215
|
-
const cleaned = {};
|
|
1216
|
-
for (const [k, v] of Object.entries(prop)) if (!TYPE_DEPENDENT.has(k)) cleaned[k] = v;
|
|
1217
|
-
normalizedProps[key] = Object.keys(cleaned).length > 0 ? cleaned : {};
|
|
1218
|
-
}
|
|
1235
|
+
if (KEEP_AS_IS.has(key)) continue;
|
|
1236
|
+
normalizedProps[key] = {};
|
|
1219
1237
|
}
|
|
1220
1238
|
const normalizedSchema = {
|
|
1221
1239
|
...listQuerySchema,
|
package/dist/docs/index.d.mts
CHANGED
package/dist/dynamic/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Vt as ResourceDefinition, r as DataAdapter } from "../interface-DYH8AXGe.mjs";
|
|
2
2
|
import { t as PermissionCheck } from "../types-BNUccdcf.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/dynamic/ArcDynamicLoader.d.ts
|
package/dist/dynamic/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as ArcQueryParser } from "../queryParser-CgCtsjti.mjs";
|
|
2
|
-
import { n as defineResource } from "../defineResource-
|
|
2
|
+
import { n as defineResource } from "../defineResource-wWMBB4GP.mjs";
|
|
3
3
|
import { S as readOnly, _ as fullPublic, b as publicRead, g as authenticated, h as adminOnly, v as ownerWithAdminBypass, x as publicReadAdminWrite } from "../permissions-C8ImI8gC.mjs";
|
|
4
4
|
//#region src/dynamic/ArcDynamicLoader.ts
|
|
5
5
|
const VALID_FIELD_TYPES = new Set([
|
package/dist/factory/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-
|
|
1
|
+
import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-By-5mIfn.mjs";
|
|
2
2
|
import { FastifyInstance } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/factory/createApp.d.ts
|