@classytic/arc 2.19.0 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/arc.js +37 -0
- package/dist/{BaseController-Dy6EQHjM.mjs → BaseController-D-MHZ27_.mjs} +4 -4
- package/dist/{ResourceRegistry-5fKZwhK_.mjs → ResourceRegistry-7RnrEWNP.mjs} +1 -1
- package/dist/{actionPermissions-D7NGwcAR.mjs → actionPermissions-C9RMiIpc.mjs} +1 -1
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +7 -5
- package/dist/{buildHandler-B0qvAguf.mjs → buildHandler-D_2ULWxZ.mjs} +3 -3
- package/dist/cache/index.mjs +2 -2
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.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 +1 -1
- package/dist/cli/commands/introspect.mjs +4 -13
- package/dist/cli/index.mjs +2 -2
- package/dist/{constants-gaEeMlz-.mjs → constants-DNre0wRg.mjs} +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +4 -4
- package/dist/{core-Ba-6Sue6.mjs → core-CWhyGkiM.mjs} +125 -53
- package/dist/{createActionRouter-B_WzBFyf.mjs → createActionRouter-HlQso0jO.mjs} +4 -4
- package/dist/{createAggregationRouter-B_G_nIjm.mjs → createAggregationRouter-5LzPzgw1.mjs} +5 -4
- package/dist/{createApp--703hQE1.mjs → createApp-D6uwzCKu.mjs} +210 -17
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/events/index.mjs +9 -6
- package/dist/factory/index.d.mts +3 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fastify-augmentation-D-5_yQJi.d.mts → fastify-augmentation-CCQJEDgh.d.mts} +12 -1
- package/dist/{generate-Brans3fW.mjs → generate-usRSdQmP.mjs} +4 -3
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +1 -1
- package/dist/{index-CRKiiP5o.d.mts → index-CdFkIMEd.d.mts} +5 -0
- package/dist/{index-CP6A1Ffj.d.mts → index-DUa8WHz3.d.mts} +1 -1
- package/dist/{index-CUTG2Cuo.d.mts → index-_--GyzcO.d.mts} +28 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +6 -6
- package/dist/{init-Bqpt1Hxf.mjs → init-YH_nslQR.mjs} +87 -5
- 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/integrations/streamline.d.mts +17 -5
- package/dist/integrations/streamline.mjs +9 -5
- package/dist/{loadResourcesFromEntry-BTlFEx05.mjs → loadResourcesFromEntry-OMCvIEE7.mjs} +1 -1
- package/dist/{mcpPlugin-C088Nd-G.mjs → mcpPlugin-CuW6L_uD.mjs} +148 -49
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/module-JYH9V0I8.d.mts +384 -0
- package/dist/{openapi-DZ-5a5Op.mjs → openapi-Dq6RbEGK.mjs} +3 -3
- package/dist/permissions/index.d.mts +1 -1
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-BPB1xqGf.mjs → permissions-CgbOLJZG.mjs} +10 -2
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +10 -5
- package/dist/plugins/index.mjs +3 -3
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{pluralize-DccFV9R7.mjs → pluralize-BhrgwZb9.mjs} +3 -3
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +16 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +1 -1
- package/dist/presets/search.mjs +3 -2
- package/dist/{presets-BtyFwTO4.mjs → presets-DWsYQdbV.mjs} +1 -1
- package/dist/{queryCachePlugin-emHb0CuX.mjs → queryCachePlugin-DzqLN904.mjs} +2 -2
- package/dist/{registerRoute-BdV8DDwU.mjs → registerRoute-DGSREwPw.mjs} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{registry-Bm9LjWKs.mjs → registry-vcOCtPij.mjs} +1 -1
- package/dist/{schemaIR-D1MJwqmp.mjs → schemaIR-ajetw3Fv.mjs} +1 -42
- package/dist/scim/index.mjs +2 -2
- package/dist/sync/changelog.d.mts +2 -0
- package/dist/sync/changelog.mjs +2 -0
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +2 -2
- package/dist/types/index.d.mts +1 -1
- package/dist/{types-9_wtNPsI.d.mts → types-BHMKlMBU.d.mts} +53 -195
- package/dist/{types-DpvSF5Hh.d.mts → types-C9b2HR5W.d.mts} +1 -1
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-C6QQ9-hd.mjs → utils-ChjYQpjn.mjs} +1400 -1400
- package/dist/{validate-CRcaWzp2.mjs → validate-C2mGgSXg.mjs} +3 -3
- package/package.json +13 -8
- package/skills/arc/SKILL.md +10 -0
- package/skills/arc/references/mcp.md +2 -0
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ export default defineResource({
|
|
|
126
126
|
});
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
Auto-generates: `GET /products`, `GET /products/:id`, `POST /products`, `PATCH /products/:id`, `DELETE /products/:id` + softDelete adds `GET /products/deleted`, `POST /products/:id/restore` + slugLookup adds `GET /products/
|
|
129
|
+
Auto-generates: `GET /products`, `GET /products/:id`, `POST /products`, `PATCH /products/:id`, `DELETE /products/:id` + softDelete adds `GET /products/deleted`, `POST /products/:id/restore` + slugLookup adds `GET /products/slug/:slug` + custom routes + `POST /products/:id/action`.
|
|
130
130
|
|
|
131
131
|
### Resource shorthands
|
|
132
132
|
|
package/bin/arc.js
CHANGED
|
@@ -60,8 +60,30 @@ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
|
60
60
|
|
|
61
61
|
const [command, subcommand, ...rest] = args;
|
|
62
62
|
|
|
63
|
+
// Per-command usage — `arc <command> --help` prints this instead of falling
|
|
64
|
+
// through to the command (where `--help` was mistaken for an argument).
|
|
65
|
+
const COMMAND_ALIASES = { new: 'init', g: 'generate', i: 'introspect', desc: 'describe', d: 'docs' };
|
|
66
|
+
const COMMAND_HELP = {
|
|
67
|
+
init: 'Usage: arc init [name] [--jwt|--better-auth] [--mongokit|--custom] [--api-key]\n [--multi|--single] [--js] [--edge] [--docker|--no-docker] [--skip-install] [--force]\n Scaffold a new arc project (prompts when run in a TTY without a name).',
|
|
68
|
+
generate: 'Usage: arc generate <resource|controller|model|repository|schemas|mcp> <name> [--mcp]\n Aliases: g; types r/c/m/repo/s. Name must be kebab-case (letters, digits, hyphens).',
|
|
69
|
+
introspect: 'Usage: arc introspect <entry-file>\n Print a human-readable summary of the resources an entry file exports.',
|
|
70
|
+
describe: 'Usage: arc describe <entry-file> [--json] [--pretty] [--entry <file>]\n Describe resources; --json emits machine output for AI agents.',
|
|
71
|
+
docs: 'Usage: arc docs <entry-file> [output.json] [--entry <file>]\n Export the generated OpenAPI document.',
|
|
72
|
+
doctor: 'Usage: arc doctor\n Diagnose common arc setup issues (exit 1 on hard failures).',
|
|
73
|
+
};
|
|
74
|
+
|
|
63
75
|
async function main() {
|
|
64
76
|
try {
|
|
77
|
+
// `arc <command> --help` / `-h` → command-specific usage, exit 0.
|
|
78
|
+
const canonicalCommand = COMMAND_ALIASES[command] ?? command;
|
|
79
|
+
if (
|
|
80
|
+
COMMAND_HELP[canonicalCommand] &&
|
|
81
|
+
[subcommand, ...rest].some((a) => a === '--help' || a === '-h')
|
|
82
|
+
) {
|
|
83
|
+
console.log(COMMAND_HELP[canonicalCommand]);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
switch (command) {
|
|
66
88
|
case 'init':
|
|
67
89
|
case 'new':
|
|
@@ -310,6 +332,21 @@ function parseInitOptions(rawArgs) {
|
|
|
310
332
|
case '-f':
|
|
311
333
|
opts.force = true;
|
|
312
334
|
break;
|
|
335
|
+
|
|
336
|
+
case '--help':
|
|
337
|
+
case '-h':
|
|
338
|
+
// Handled by the per-command help short-circuit in main(); ignore here.
|
|
339
|
+
break;
|
|
340
|
+
|
|
341
|
+
default:
|
|
342
|
+
// Surface typos instead of silently scaffolding with defaults — a
|
|
343
|
+
// dropped `--jwt` / `--mongokit` would change the whole project. Warn
|
|
344
|
+
// (non-fatal, to stderr) rather than error, since a scaffolder should
|
|
345
|
+
// stay forgiving.
|
|
346
|
+
if (arg.startsWith('-')) {
|
|
347
|
+
console.error(`Warning: unknown flag "${arg}" (ignored). Run "arc init --help".`);
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
313
350
|
}
|
|
314
351
|
}
|
|
315
352
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-
|
|
1
|
+
import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-DNre0wRg.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { d as createDomainError, f as createError, i as NotFoundError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
4
4
|
import { b as isElevated, c as getOrgId, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { F as applyFieldWritePermissions, L as resolveEffectiveRoles } from "./permissions-BPB1xqGf.mjs";
|
|
5
|
+
import { N as simpleEqualityMatcher, _ as ArcQueryParser, r as scheduleBackground, t as getUserId } from "./utils-ChjYQpjn.mjs";
|
|
6
|
+
import { F as applyFieldWritePermissions, L as resolveEffectiveRoles } from "./permissions-CgbOLJZG.mjs";
|
|
8
7
|
import { t as getUserRoles } from "./types-XTApa8b6.mjs";
|
|
8
|
+
import { t as buildQueryKey } from "./keys-Cd316u_y.mjs";
|
|
9
9
|
//#region src/core/AccessControl.ts
|
|
10
10
|
const log = arcLog("access-control");
|
|
11
11
|
var AccessControl = class {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { b as isElevated, c as getOrgId, g as getUserId, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
|
|
2
|
-
import { L as resolveEffectiveRoles, P as applyFieldReadPermissions } from "./permissions-
|
|
2
|
+
import { L as resolveEffectiveRoles, P as applyFieldReadPermissions } from "./permissions-CgbOLJZG.mjs";
|
|
3
3
|
import { t as getUserRoles } from "./types-XTApa8b6.mjs";
|
|
4
4
|
import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
|
|
5
5
|
import { isPaginatedResult, toCanonicalList } from "@classytic/repo-core/pagination";
|
package/dist/audit/index.mjs
CHANGED
|
@@ -108,7 +108,7 @@ function extractUserId(user) {
|
|
|
108
108
|
*/
|
|
109
109
|
function detectChanges(before, after) {
|
|
110
110
|
const changes = [];
|
|
111
|
-
const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
111
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
112
112
|
for (const key of allKeys) {
|
|
113
113
|
if (key.startsWith("_") || key === "updatedAt") continue;
|
|
114
114
|
if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) changes.push(key);
|
package/dist/auth/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ht as AuthPluginOptions, Vt as AuthHelpers } from "../index-
|
|
1
|
+
import { Ht as AuthPluginOptions, Vt as AuthHelpers } from "../index-_--GyzcO.mjs";
|
|
2
2
|
import { c as PermissionCheck } from "../fields-C7ahdGYf.mjs";
|
|
3
3
|
import { t as ExternalOpenApiPaths } from "../externalPaths-BE6wFpYv.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-BXG-HGwG.mjs";
|
package/dist/auth/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-DkO8Nk6-.mjs";
|
|
2
|
-
import { _ as requireTeamMembership, m as requireOrgRole, p as requireOrgMembership } from "../permissions-
|
|
2
|
+
import { _ as requireTeamMembership, m as requireOrgRole, p as requireOrgMembership } from "../permissions-CgbOLJZG.mjs";
|
|
3
3
|
import { n as normalizeRoles, t as getUserRoles } from "../types-XTApa8b6.mjs";
|
|
4
4
|
import { a as toJsonSchema } from "../schemaConverter-U0MnO9vd.mjs";
|
|
5
5
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -1054,7 +1054,7 @@ var MemorySessionStore = class {
|
|
|
1054
1054
|
const userSessions = this.userIndex.get(userId);
|
|
1055
1055
|
if (!userSessions) return;
|
|
1056
1056
|
for (const sessionId of userSessions) if (sessionId !== currentSessionId) this.sessions.delete(sessionId);
|
|
1057
|
-
if (userSessions.has(currentSessionId)) this.userIndex.set(userId, new Set([currentSessionId]));
|
|
1057
|
+
if (userSessions.has(currentSessionId)) this.userIndex.set(userId, /* @__PURE__ */ new Set([currentSessionId]));
|
|
1058
1058
|
else this.userIndex.delete(userId);
|
|
1059
1059
|
}
|
|
1060
1060
|
/**
|
|
@@ -1160,9 +1160,10 @@ function createSessionManager(options) {
|
|
|
1160
1160
|
metadata
|
|
1161
1161
|
};
|
|
1162
1162
|
await store.set(sessionId, sessionData);
|
|
1163
|
+
const signedId = signSessionId(sessionId, secret);
|
|
1163
1164
|
return {
|
|
1164
1165
|
sessionId,
|
|
1165
|
-
cookie: buildSetCookieHeader(cookieName,
|
|
1166
|
+
cookie: buildSetCookieHeader(cookieName, signedId, maxAgeSeconds, cookieOptions)
|
|
1166
1167
|
};
|
|
1167
1168
|
}
|
|
1168
1169
|
/**
|
|
@@ -1257,7 +1258,8 @@ function createSessionManager(options) {
|
|
|
1257
1258
|
if (Date.now() - session.updatedAt > updateAgeMs) {
|
|
1258
1259
|
const updatedSession = await refreshSession(sessionId);
|
|
1259
1260
|
if (updatedSession) {
|
|
1260
|
-
const
|
|
1261
|
+
const signedId = signSessionId(sessionId, secret);
|
|
1262
|
+
const newCookie = buildSetCookieHeader(cookieName, signedId, maxAgeSeconds, cookieOptions);
|
|
1261
1263
|
reply.header("Set-Cookie", newCookie);
|
|
1262
1264
|
request.session = {
|
|
1263
1265
|
...updatedSession,
|
|
@@ -1300,7 +1302,7 @@ function mirrorTrustedOriginsFromCors(options) {
|
|
|
1300
1302
|
const { corsOrigins, canonicalUrl } = options;
|
|
1301
1303
|
if (corsOrigins === true) return ["*"];
|
|
1302
1304
|
if (!corsOrigins) return [canonicalUrl];
|
|
1303
|
-
return Array.from(new Set([canonicalUrl, ...corsOrigins]));
|
|
1305
|
+
return Array.from(/* @__PURE__ */ new Set([canonicalUrl, ...corsOrigins]));
|
|
1304
1306
|
}
|
|
1305
1307
|
//#endregion
|
|
1306
1308
|
export { MemorySessionStore, authPlugin_default as authPlugin, authPlugin as authPluginFn, createBetterAuthAdapter, createSessionManager, extractBetterAuthOpenApi, mirrorTrustedOriginsFromCors };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as compileAggRequest, t as adapterSupportsAggregate } from "./validate-
|
|
1
|
+
import { i as compileAggRequest, t as adapterSupportsAggregate } from "./validate-C2mGgSXg.mjs";
|
|
2
2
|
//#region src/core/aggregation/buildHandler.ts
|
|
3
3
|
/**
|
|
4
4
|
* Framework-agnostic aggregation execution. Runs safety guards,
|
|
@@ -193,7 +193,7 @@ const OPERATOR_SHORTHAND = {
|
|
|
193
193
|
size: "$size",
|
|
194
194
|
type: "$type"
|
|
195
195
|
};
|
|
196
|
-
const SHORTHAND_RANGE_OPS = new Set([
|
|
196
|
+
const SHORTHAND_RANGE_OPS = /* @__PURE__ */ new Set([
|
|
197
197
|
"gt",
|
|
198
198
|
"gte",
|
|
199
199
|
"lt",
|
|
@@ -239,7 +239,7 @@ function expandShorthandOperators(value) {
|
|
|
239
239
|
*/
|
|
240
240
|
function extractCallerFilter(query) {
|
|
241
241
|
const out = {};
|
|
242
|
-
const reserved = new Set([
|
|
242
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
243
243
|
"page",
|
|
244
244
|
"limit",
|
|
245
245
|
"after",
|
package/dist/cache/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-Cd316u_y.mjs";
|
|
2
1
|
import { t as MemoryCacheStore } from "../memory-CLXA2VeJ.mjs";
|
|
3
|
-
import { r as
|
|
2
|
+
import { i as versionKey, n as hashParams, r as tagVersionKey, t as buildQueryKey } from "../keys-Cd316u_y.mjs";
|
|
3
|
+
import { r as QueryCache, t as queryCachePlugin } from "../queryCachePlugin-DzqLN904.mjs";
|
|
4
4
|
//#region src/cache/redis.ts
|
|
5
5
|
/**
|
|
6
6
|
* Redis-backed cache store.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { V as ResourceDefinition, ht as RouteSchemaOptions, it as RateLimitConfig } from "../../index-
|
|
1
|
+
import { V as ResourceDefinition, ht as RouteSchemaOptions, it as RateLimitConfig } from "../../index-_--GyzcO.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/cli/commands/describe.d.ts
|
|
4
4
|
interface DescribedResource {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as CRUD_OPERATIONS } from "../../constants-
|
|
2
|
-
import { n as stringifyMeasureMap } from "../../ResourceRegistry-
|
|
1
|
+
import { t as CRUD_OPERATIONS } from "../../constants-DNre0wRg.mjs";
|
|
2
|
+
import { n as stringifyMeasureMap } from "../../ResourceRegistry-7RnrEWNP.mjs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
//#region src/cli/commands/describe.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as buildOpenApiSpec } from "../../openapi-
|
|
2
|
-
import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-
|
|
1
|
+
import { t as buildOpenApiSpec } from "../../openapi-Dq6RbEGK.mjs";
|
|
2
|
+
import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-OMCvIEE7.mjs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
//#region src/cli/commands/docs.ts
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as generate } from "../../generate-
|
|
1
|
+
import { t as generate } from "../../generate-usRSdQmP.mjs";
|
|
2
2
|
export { generate };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as init } from "../../init-
|
|
1
|
+
import { t as init } from "../../init-YH_nslQR.mjs";
|
|
2
2
|
export { init };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-
|
|
1
|
+
import { t as loadResourcesFromEntry } from "../../loadResourcesFromEntry-OMCvIEE7.mjs";
|
|
2
2
|
//#region src/cli/commands/introspect.ts
|
|
3
3
|
function describePermission(value) {
|
|
4
4
|
if (!value) return "none";
|
|
@@ -10,19 +10,10 @@ function describePermission(value) {
|
|
|
10
10
|
async function introspect(args) {
|
|
11
11
|
console.log("Introspecting Arc resources...\n");
|
|
12
12
|
const entryPath = args[0];
|
|
13
|
-
if (!entryPath)
|
|
14
|
-
console.log("Usage: arc introspect <entry-file>\n");
|
|
15
|
-
console.log("Where entry-file exports your defineResource() results.");
|
|
16
|
-
console.log("Example: arc introspect ./src/resources.js");
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
13
|
+
if (!entryPath) throw new Error("Missing entry-file argument\nUsage: arc introspect <entry-file>\nWhere entry-file exports your defineResource() results.\nExample: arc introspect ./src/resources.js");
|
|
19
14
|
const { registry, registered } = await loadResourcesFromEntry(entryPath);
|
|
20
|
-
if (registered === 0) {
|
|
21
|
-
|
|
22
|
-
console.log("\nMake sure your file exports defineResource() results:");
|
|
23
|
-
console.log(" export const productResource = defineResource({ ... });");
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
15
|
+
if (registered === 0) throw new Error(`No resource definitions found in "${entryPath}".\nMake sure the file exports defineResource() results:
|
|
16
|
+
export const productResource = defineResource({ ... });`);
|
|
26
17
|
const resources = registry.getAll();
|
|
27
18
|
console.log(`Found ${resources.length} resource(s):\n`);
|
|
28
19
|
resources.forEach((resource, index) => {
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe } from "./commands/describe.mjs";
|
|
2
2
|
import { exportDocs } from "./commands/docs.mjs";
|
|
3
3
|
import { doctor } from "./commands/doctor.mjs";
|
|
4
|
-
import { t as generate } from "../generate-
|
|
5
|
-
import { t as init } from "../init-
|
|
4
|
+
import { t as generate } from "../generate-usRSdQmP.mjs";
|
|
5
|
+
import { t as init } from "../init-YH_nslQR.mjs";
|
|
6
6
|
import { introspect } from "./commands/introspect.mjs";
|
|
7
7
|
export { describe, doctor, exportDocs, generate, init, introspect };
|
|
@@ -73,7 +73,7 @@ const MAX_FILTER_DEPTH = 10;
|
|
|
73
73
|
* filter. See `ArcQueryParser.parseFilters()` for the unwrap step that
|
|
74
74
|
* merges `q.filter[*]` back into the top-level filter map.
|
|
75
75
|
*/
|
|
76
|
-
const RESERVED_QUERY_PARAMS = Object.freeze(new Set([
|
|
76
|
+
const RESERVED_QUERY_PARAMS = Object.freeze(/* @__PURE__ */ new Set([
|
|
77
77
|
"page",
|
|
78
78
|
"limit",
|
|
79
79
|
"sort",
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { At as AggregationMaterializedResult, B as defineResource, Bn as BodySanitizerConfig, Ct as AggMeasureInput, Dt as AggregationDateRangeRequirement, Et as AggregationConfig, Fn as BaseControllerOptions, In as ControllerConfigurableOptions, Ln as ControllerConstructionOptions, Mt as AggregationsMap, Ot as AggregationIndexHint, Rn as ListResult, Tt as AggregationCacheConfig, V as ResourceDefinition, an as SlugExt, cn as BulkMixin, dn as QueryResolverConfig, en as BaseController, fn as AccessControl, in as TreeMixin, jt as AggregationRateLimit, kt as AggregationMaterializedContext, ln as BaseCrudController, nn as SoftDeleteMixin, on as SlugMixin, pn as AccessControlConfig, rn as TreeExt, sn as BulkExt, tn as SoftDeleteExt, un as QueryResolver, wt as AggMeasureShorthand, zn as BodySanitizer } from "../index-
|
|
2
|
-
import { A as MAX_FILTER_DEPTH, C as DEFAULT_SORT, D as HOOK_PHASES, E as HOOK_OPERATIONS, F as RESERVED_QUERY_PARAMS, I as SYSTEM_FIELDS, M as MAX_SEARCH_LENGTH, N as MUTATION_OPERATIONS, O as HookOperation, P as MutationOperation, S as DEFAULT_MAX_LIMIT, T as DEFAULT_UPDATE_METHOD, _ as defineAggregation, a as createRequestContext, b as DEFAULT_ID_FIELD, c as sendControllerResponse, d as getEntityQuery, f as defineResourceVariants, g as createPermissionMiddleware, h as createCrudRouter, i as createFastifyHandler, j as MAX_REGEX_LENGTH, k as HookPhase, l as getEntityId, m as defineAction, n as isFieldReadable, o as getControllerContext, p as DefineActionConfig, r as createCrudHandlers, s as getControllerScope, t as collectReadBlockedFields, u as getEntityIdField, v as CRUD_OPERATIONS, w as DEFAULT_TENANT_FIELD, x as DEFAULT_LIMIT, y as CrudOperation } from "../index-
|
|
1
|
+
import { At as AggregationMaterializedResult, B as defineResource, Bn as BodySanitizerConfig, Ct as AggMeasureInput, Dt as AggregationDateRangeRequirement, Et as AggregationConfig, Fn as BaseControllerOptions, In as ControllerConfigurableOptions, Ln as ControllerConstructionOptions, Mt as AggregationsMap, Ot as AggregationIndexHint, Rn as ListResult, Tt as AggregationCacheConfig, V as ResourceDefinition, an as SlugExt, cn as BulkMixin, dn as QueryResolverConfig, en as BaseController, fn as AccessControl, in as TreeMixin, jt as AggregationRateLimit, kt as AggregationMaterializedContext, ln as BaseCrudController, nn as SoftDeleteMixin, on as SlugMixin, pn as AccessControlConfig, rn as TreeExt, sn as BulkExt, tn as SoftDeleteExt, un as QueryResolver, wt as AggMeasureShorthand, zn as BodySanitizer } from "../index-_--GyzcO.mjs";
|
|
2
|
+
import { A as MAX_FILTER_DEPTH, C as DEFAULT_SORT, D as HOOK_PHASES, E as HOOK_OPERATIONS, F as RESERVED_QUERY_PARAMS, I as SYSTEM_FIELDS, M as MAX_SEARCH_LENGTH, N as MUTATION_OPERATIONS, O as HookOperation, P as MutationOperation, S as DEFAULT_MAX_LIMIT, T as DEFAULT_UPDATE_METHOD, _ as defineAggregation, a as createRequestContext, b as DEFAULT_ID_FIELD, c as sendControllerResponse, d as getEntityQuery, f as defineResourceVariants, g as createPermissionMiddleware, h as createCrudRouter, i as createFastifyHandler, j as MAX_REGEX_LENGTH, k as HookPhase, l as getEntityId, m as defineAction, n as isFieldReadable, o as getControllerContext, p as DefineActionConfig, r as createCrudHandlers, s as getControllerScope, t as collectReadBlockedFields, u as getEntityIdField, v as CRUD_OPERATIONS, w as DEFAULT_TENANT_FIELD, x as DEFAULT_LIMIT, y as CrudOperation } from "../index-DUa8WHz3.mjs";
|
|
3
3
|
export { AccessControl, type AccessControlConfig, type AggMeasureInput, type AggMeasureShorthand, type AggregationCacheConfig, type AggregationConfig, type AggregationDateRangeRequirement, type AggregationIndexHint, type AggregationMaterializedContext, type AggregationMaterializedResult, type AggregationRateLimit, type AggregationsMap, BaseController, type BaseControllerOptions, BaseCrudController, BodySanitizer, type BodySanitizerConfig, type BulkExt, BulkMixin, CRUD_OPERATIONS, type ControllerConfigurableOptions, type ControllerConstructionOptions, CrudOperation, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, type DefineActionConfig, HOOK_OPERATIONS, HOOK_PHASES, HookOperation, HookPhase, type ListResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MutationOperation, QueryResolver, type QueryResolverConfig, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, type SlugExt, SlugMixin, type SoftDeleteExt, SoftDeleteMixin, type TreeExt, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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-
|
|
2
|
-
import { a as BulkMixin, c as collectReadBlockedFields, f as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController-
|
|
3
|
-
import { a as defineResource, c as createCrudRouter, i as defineResourceVariants, l as createPermissionMiddleware, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as defineAction, t as getEntityId, u as defineAggregation } from "../core-
|
|
4
|
-
import { a as createCrudHandlers, c as getControllerContext, l as getControllerScope, o as createFastifyHandler, s as createRequestContext, u as sendControllerResponse } from "../actionPermissions-
|
|
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-DNre0wRg.mjs";
|
|
2
|
+
import { a as BulkMixin, c as collectReadBlockedFields, f as AccessControl, i as SlugMixin, l as isFieldReadable, n as TreeMixin, o as BaseCrudController, r as SoftDeleteMixin, s as QueryResolver, t as BaseController, u as BodySanitizer } from "../BaseController-D-MHZ27_.mjs";
|
|
3
|
+
import { a as defineResource, c as createCrudRouter, i as defineResourceVariants, l as createPermissionMiddleware, n as getEntityIdField, o as ResourceDefinition, r as getEntityQuery, s as defineAction, t as getEntityId, u as defineAggregation } from "../core-CWhyGkiM.mjs";
|
|
4
|
+
import { a as createCrudHandlers, c as getControllerContext, l as getControllerScope, o as createFastifyHandler, s as createRequestContext, u as sendControllerResponse } from "../actionPermissions-C9RMiIpc.mjs";
|
|
5
5
|
export { AccessControl, BaseController, BaseCrudController, BodySanitizer, BulkMixin, 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, SlugMixin, SoftDeleteMixin, TreeMixin, collectReadBlockedFields, createCrudHandlers, createCrudRouter, createFastifyHandler, createPermissionMiddleware, createRequestContext, defineAction, defineAggregation, defineResource, defineResourceVariants, getControllerContext, getControllerScope, getEntityId, getEntityIdField, getEntityQuery, isFieldReadable, sendControllerResponse };
|
|
@@ -1,18 +1,57 @@
|
|
|
1
|
-
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-
|
|
1
|
+
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-DNre0wRg.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
4
4
|
import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-
|
|
7
|
-
import {
|
|
5
|
+
import { A as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-ChjYQpjn.mjs";
|
|
6
|
+
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-D-MHZ27_.mjs";
|
|
7
|
+
import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, l as resolveRoutePreHandlers, n as buildRateLimitConfig, r as buildRouteConfig, s as buildCrudPermissionMw, t as tryRegisterRoute, u as buildAuthMiddleware } from "./registerRoute-DGSREwPw.mjs";
|
|
8
|
+
import { F as applyFieldWritePermissions, L as resolveEffectiveRoles } from "./permissions-CgbOLJZG.mjs";
|
|
8
9
|
import { t as getUserRoles } from "./types-XTApa8b6.mjs";
|
|
10
|
+
import { t as applyPresets } from "./presets-DWsYQdbV.mjs";
|
|
9
11
|
import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-U0MnO9vd.mjs";
|
|
10
12
|
import { i as pipeUIMessageStreamToReply, r as isReadableStream } from "./streaming-BQcW6_Kq.mjs";
|
|
11
|
-
import { a as createCrudHandlers, d as buildRequestScopeProjection, i as resolvePipelineSteps, o as createFastifyHandler, r as buildPipelineHandler, s as createRequestContext, t as resolveActionPermission } from "./actionPermissions-D7NGwcAR.mjs";
|
|
12
|
-
import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, l as resolveRoutePreHandlers, n as buildRateLimitConfig, r as buildRouteConfig, s as buildCrudPermissionMw, t as tryRegisterRoute, u as buildAuthMiddleware } from "./registerRoute-BdV8DDwU.mjs";
|
|
13
|
-
import { t as applyPresets } from "./presets-BtyFwTO4.mjs";
|
|
14
13
|
import { t as hasEvents } from "./typeGuards-C9KXUly6.mjs";
|
|
15
|
-
import { t as
|
|
14
|
+
import { a as createCrudHandlers, d as buildRequestScopeProjection, i as resolvePipelineSteps, o as createFastifyHandler, r as buildPipelineHandler, s as createRequestContext, t as resolveActionPermission } from "./actionPermissions-C9RMiIpc.mjs";
|
|
15
|
+
import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
|
|
16
|
+
//#region src/core/middlewares/fieldWrite.ts
|
|
17
|
+
/**
|
|
18
|
+
* Build a preHandler that enforces field-write permissions on the request body.
|
|
19
|
+
*
|
|
20
|
+
* Auto-CRUD's create/update routes get this for free via `BodySanitizer.sanitize()`
|
|
21
|
+
* inside `BaseController`. Custom routes (`config.routes`, presets, action endpoints)
|
|
22
|
+
* never went through that path — so a host that declared
|
|
23
|
+
* `fields: { role: fields.writableBy(['admin']) }` and a custom
|
|
24
|
+
* `POST /users/promote` happily accepted `{ role: 'admin' }` from any caller.
|
|
25
|
+
*
|
|
26
|
+
* This preHandler closes that gap. It applies ONLY the `applyFieldWritePermissions`
|
|
27
|
+
* step — no `SYSTEM_FIELDS` strip, no `fieldRules` strip — because custom-route
|
|
28
|
+
* bodies don't necessarily mirror the resource's adapter shape, and we'd
|
|
29
|
+
* generate surprising 403s by enforcing rules tied to that shape.
|
|
30
|
+
*
|
|
31
|
+
* Returns `null` when the resource has no field permissions configured — the
|
|
32
|
+
* caller should then skip wiring it into the preHandler chain.
|
|
33
|
+
*/
|
|
34
|
+
function buildFieldWritePreHandler(fieldPermissions, policy = DEFAULT_FIELD_WRITE_DENIAL_POLICY) {
|
|
35
|
+
if (!fieldPermissions || Object.keys(fieldPermissions).length === 0) return null;
|
|
36
|
+
return async (request, _reply) => {
|
|
37
|
+
const body = request.body;
|
|
38
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return;
|
|
39
|
+
const scope = request.scope ?? PUBLIC_SCOPE;
|
|
40
|
+
if (isElevated(scope)) return;
|
|
41
|
+
const { body: filtered, deniedFields } = applyFieldWritePermissions(body, fieldPermissions, resolveEffectiveRoles(getUserRoles(request.user), isMember(scope) ? scope.orgRoles : []));
|
|
42
|
+
if (deniedFields.length > 0 && policy === "reject") throw new ForbiddenError(`Not permitted to write field${deniedFields.length === 1 ? "" : "s"}: ${deniedFields.join(", ")}`);
|
|
43
|
+
request.body = filtered;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* True for HTTP methods that carry a request body and should run the
|
|
48
|
+
* field-write preHandler. GET/DELETE/HEAD/OPTIONS skip enforcement.
|
|
49
|
+
*/
|
|
50
|
+
function methodCarriesBody(method) {
|
|
51
|
+
const m = method.toUpperCase();
|
|
52
|
+
return m === "POST" || m === "PUT" || m === "PATCH";
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
16
55
|
//#region src/core/aggregation/defineAggregation.ts
|
|
17
56
|
/**
|
|
18
57
|
* Declare a single resource aggregation. Exported configs flow into
|
|
@@ -59,45 +98,6 @@ function defineAggregation(config) {
|
|
|
59
98
|
return config;
|
|
60
99
|
}
|
|
61
100
|
//#endregion
|
|
62
|
-
//#region src/core/middlewares/fieldWrite.ts
|
|
63
|
-
/**
|
|
64
|
-
* Build a preHandler that enforces field-write permissions on the request body.
|
|
65
|
-
*
|
|
66
|
-
* Auto-CRUD's create/update routes get this for free via `BodySanitizer.sanitize()`
|
|
67
|
-
* inside `BaseController`. Custom routes (`config.routes`, presets, action endpoints)
|
|
68
|
-
* never went through that path — so a host that declared
|
|
69
|
-
* `fields: { role: fields.writableBy(['admin']) }` and a custom
|
|
70
|
-
* `POST /users/promote` happily accepted `{ role: 'admin' }` from any caller.
|
|
71
|
-
*
|
|
72
|
-
* This preHandler closes that gap. It applies ONLY the `applyFieldWritePermissions`
|
|
73
|
-
* step — no `SYSTEM_FIELDS` strip, no `fieldRules` strip — because custom-route
|
|
74
|
-
* bodies don't necessarily mirror the resource's adapter shape, and we'd
|
|
75
|
-
* generate surprising 403s by enforcing rules tied to that shape.
|
|
76
|
-
*
|
|
77
|
-
* Returns `null` when the resource has no field permissions configured — the
|
|
78
|
-
* caller should then skip wiring it into the preHandler chain.
|
|
79
|
-
*/
|
|
80
|
-
function buildFieldWritePreHandler(fieldPermissions, policy = DEFAULT_FIELD_WRITE_DENIAL_POLICY) {
|
|
81
|
-
if (!fieldPermissions || Object.keys(fieldPermissions).length === 0) return null;
|
|
82
|
-
return async (request, _reply) => {
|
|
83
|
-
const body = request.body;
|
|
84
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) return;
|
|
85
|
-
const scope = request.scope ?? PUBLIC_SCOPE;
|
|
86
|
-
if (isElevated(scope)) return;
|
|
87
|
-
const { body: filtered, deniedFields } = applyFieldWritePermissions(body, fieldPermissions, resolveEffectiveRoles(getUserRoles(request.user), isMember(scope) ? scope.orgRoles : []));
|
|
88
|
-
if (deniedFields.length > 0 && policy === "reject") throw new ForbiddenError(`Not permitted to write field${deniedFields.length === 1 ? "" : "s"}: ${deniedFields.join(", ")}`);
|
|
89
|
-
request.body = filtered;
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* True for HTTP methods that carry a request body and should run the
|
|
94
|
-
* field-write preHandler. GET/DELETE/HEAD/OPTIONS skip enforcement.
|
|
95
|
-
*/
|
|
96
|
-
function methodCarriesBody(method) {
|
|
97
|
-
const m = method.toUpperCase();
|
|
98
|
-
return m === "POST" || m === "PUT" || m === "PATCH";
|
|
99
|
-
}
|
|
100
|
-
//#endregion
|
|
101
101
|
//#region src/core/createCrudRouter.ts
|
|
102
102
|
/**
|
|
103
103
|
* Mount custom routes (from presets or user-defined `routes`) on Fastify.
|
|
@@ -177,7 +177,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
177
177
|
}
|
|
178
178
|
return result;
|
|
179
179
|
} : handler,
|
|
180
|
-
...buildRouteConfig(rateLimitConfig, extensions)
|
|
180
|
+
...buildRouteConfig(route.rateLimit !== void 0 ? buildRateLimitConfig(route.rateLimit) : rateLimitConfig, extensions)
|
|
181
181
|
}, {
|
|
182
182
|
resourceName,
|
|
183
183
|
op: opName
|
|
@@ -949,6 +949,42 @@ function computeHasCrudRoutes(config) {
|
|
|
949
949
|
const disabled = new Set(config.disabledRoutes ?? []);
|
|
950
950
|
return !config.disableDefaultRoutes && CRUD_OPERATIONS.some((op) => !disabled.has(op));
|
|
951
951
|
}
|
|
952
|
+
const WRITE_OPERATIONS = /* @__PURE__ */ new Set([
|
|
953
|
+
"create",
|
|
954
|
+
"update",
|
|
955
|
+
"delete"
|
|
956
|
+
]);
|
|
957
|
+
/**
|
|
958
|
+
* Public-by-omission CRUD diagnostic (post-preset).
|
|
959
|
+
*
|
|
960
|
+
* An omitted CRUD permission mounts the route with NO permission
|
|
961
|
+
* middleware at all — unlike custom routes and actions, which fail boot
|
|
962
|
+
* without one. That asymmetry silently shipped unauthenticated writes
|
|
963
|
+
* (`permissions: readOnly()` pre-2.20 being the canonical victim).
|
|
964
|
+
*
|
|
965
|
+
* Runs against `resolvedConfig` — never the raw user config — so
|
|
966
|
+
* permissions injected by presets (ownedByUser, multiTenant policy
|
|
967
|
+
* wiring, ...) count as gates and don't false-positive.
|
|
968
|
+
*
|
|
969
|
+
* Severity ladder:
|
|
970
|
+
* - `warn` when any enabled WRITE op (create/update/delete) is ungated —
|
|
971
|
+
* this is real exposure the host must acknowledge.
|
|
972
|
+
* - `info` when only reads are ungated — public catalogs
|
|
973
|
+
* (`referenceData: true`) are a legitimate shape, but the host should
|
|
974
|
+
* still state intent with `permissions.fullPublic()` / `allowPublic()`.
|
|
975
|
+
*/
|
|
976
|
+
function collectUngatedCrudDiagnostics(config) {
|
|
977
|
+
if (config.disableDefaultRoutes) return [];
|
|
978
|
+
const disabled = new Set(config.disabledRoutes ?? []);
|
|
979
|
+
const permissions = config.permissions ?? {};
|
|
980
|
+
const ungated = CRUD_OPERATIONS.filter((op) => !disabled.has(op) && typeof permissions[op] !== "function");
|
|
981
|
+
if (ungated.length === 0) return [];
|
|
982
|
+
return [{
|
|
983
|
+
severity: ungated.some((op) => WRITE_OPERATIONS.has(op)) ? "warn" : "info",
|
|
984
|
+
code: "crud-public-by-omission",
|
|
985
|
+
message: `[Arc] Resource '${config.name}' mounts ${ungated.length} CRUD route(s) with no permission gate: ${ungated.join(", ")}.\nAn omitted permission mounts the route WITHOUT auth. State intent explicitly:\n permissions: permissions.fullPublic() // intentionally public\n permissions: permissions.authenticated() // require auth on every op\n permissions: { ${ungated[0]}: requireAuth(), ... } // per-operation`
|
|
986
|
+
}];
|
|
987
|
+
}
|
|
952
988
|
//#endregion
|
|
953
989
|
//#region src/registry/resolveTenantPurge.ts
|
|
954
990
|
const DEFAULT_PRIORITY = 100;
|
|
@@ -1102,7 +1138,7 @@ function deepMergeSchemas(base, override) {
|
|
|
1102
1138
|
if (!override) return base;
|
|
1103
1139
|
if (!base) return override;
|
|
1104
1140
|
const result = { ...base };
|
|
1105
|
-
for (const [key, value] of Object.entries(override)) if (Array.isArray(value) && Array.isArray(result[key])) result[key] = [
|
|
1141
|
+
for (const [key, value] of Object.entries(override)) if (Array.isArray(value) && Array.isArray(result[key])) result[key] = [.../* @__PURE__ */ new Set([...result[key], ...value])];
|
|
1106
1142
|
else if (value && typeof value === "object" && !Array.isArray(value)) result[key] = deepMergeSchemas(result[key], value);
|
|
1107
1143
|
else result[key] = value;
|
|
1108
1144
|
return result;
|
|
@@ -1247,7 +1283,7 @@ function buildResourcePlugin(resource) {
|
|
|
1247
1283
|
extensions: resource.extensions
|
|
1248
1284
|
});
|
|
1249
1285
|
if (resource.actions && Object.keys(resource.actions).length > 0) {
|
|
1250
|
-
const { createActionRouter } = await import("./createActionRouter-
|
|
1286
|
+
const { createActionRouter } = await import("./createActionRouter-HlQso0jO.mjs").then((n) => n.n);
|
|
1251
1287
|
createActionRouter(typedInstance, {
|
|
1252
1288
|
...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
|
|
1253
1289
|
resourceName: resource.name,
|
|
@@ -1262,7 +1298,7 @@ function buildResourcePlugin(resource) {
|
|
|
1262
1298
|
});
|
|
1263
1299
|
}
|
|
1264
1300
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
1265
|
-
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-
|
|
1301
|
+
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-5LzPzgw1.mjs"), import("./validate-C2mGgSXg.mjs").then((n) => n.r)]);
|
|
1266
1302
|
const repoForAgg = resource.controller?.repository ?? resource.repository;
|
|
1267
1303
|
if (!adapterSupportsAggregate(repoForAgg)) {
|
|
1268
1304
|
const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
|
|
@@ -1718,7 +1754,7 @@ function mergeUserOpenApiOverrides(openApiSchemas, userOverrides) {
|
|
|
1718
1754
|
* call) since the set is fixed and the cost of re-allocating is a
|
|
1719
1755
|
* pointless boot tax for hosts with hundreds of resources.
|
|
1720
1756
|
*/
|
|
1721
|
-
const CRUD_OP_NAMES = new Set([
|
|
1757
|
+
const CRUD_OP_NAMES = /* @__PURE__ */ new Set([
|
|
1722
1758
|
"create",
|
|
1723
1759
|
"update",
|
|
1724
1760
|
"delete",
|
|
@@ -1738,7 +1774,42 @@ function validateDefineResourceConfig(config) {
|
|
|
1738
1774
|
validatePermissionsShape(config);
|
|
1739
1775
|
validateCustomRoutePermissions(config);
|
|
1740
1776
|
validateActionsShape(config);
|
|
1741
|
-
return collectRedundantFieldRuleDiagnostics(config);
|
|
1777
|
+
return [...collectRedundantFieldRuleDiagnostics(config), ...collectReservedFilterNameDiagnostics(config)];
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Query-string param names arc's `QueryParser` reserves for
|
|
1781
|
+
* pagination/projection (`q.after ?? q.cursor` is the keyset cursor). A
|
|
1782
|
+
* document field with one of these names can never be filtered via the query
|
|
1783
|
+
* string — the parser consumes the name before it reaches the filter map — and
|
|
1784
|
+
* the same reservation flows into the MCP list-tool schema. Silent pre-2.20;
|
|
1785
|
+
* now surfaced.
|
|
1786
|
+
*/
|
|
1787
|
+
const RESERVED_QUERY_PARAMS = /* @__PURE__ */ new Set([
|
|
1788
|
+
"page",
|
|
1789
|
+
"limit",
|
|
1790
|
+
"cursor",
|
|
1791
|
+
"after",
|
|
1792
|
+
"sort",
|
|
1793
|
+
"search",
|
|
1794
|
+
"select",
|
|
1795
|
+
"populate"
|
|
1796
|
+
]);
|
|
1797
|
+
/**
|
|
1798
|
+
* Warn when a declared filterable field's NAME collides with a reserved query
|
|
1799
|
+
* param. Verified empirically (tests/e2e/reserved-field-name-collision.test.ts):
|
|
1800
|
+
* a field named `cursor`/`page` is silently unfilterable. Boot diagnostic, not
|
|
1801
|
+
* a hard error — the resource still works, the field just isn't query-filterable.
|
|
1802
|
+
*/
|
|
1803
|
+
function collectReservedFilterNameDiagnostics(config) {
|
|
1804
|
+
const filterable = config.schemaOptions?.filterableFields;
|
|
1805
|
+
if (!filterable) return [];
|
|
1806
|
+
const collisions = filterable.filter((f) => RESERVED_QUERY_PARAMS.has(f));
|
|
1807
|
+
if (collisions.length === 0) return [];
|
|
1808
|
+
return [{
|
|
1809
|
+
severity: "warn",
|
|
1810
|
+
code: "filter-field-reserved-name",
|
|
1811
|
+
message: `[Arc] Resource '${config.name}': filterable field(s) [${collisions.join(", ")}] collide with reserved query params (page, limit, cursor, after, sort, search, select, populate). The query parser consumes these names for pagination/projection, so filtering by them via the query string — and via the MCP list tool — will NOT work. Rename the field(s), or drop them from \`filterableFields\` if the collision is intended.`
|
|
1812
|
+
}];
|
|
1742
1813
|
}
|
|
1743
1814
|
/** Permissions must be `PermissionCheck` functions, not arbitrary values. */
|
|
1744
1815
|
function validatePermissionsShape(config) {
|
|
@@ -1820,6 +1891,7 @@ function defineResource(config) {
|
|
|
1820
1891
|
const configWithId = resolveIdField(normalisedConfig, repository);
|
|
1821
1892
|
const resolvedConfig = applyPresetsAndAutoInject(configWithId);
|
|
1822
1893
|
const hasCrudRoutes = computeHasCrudRoutes(resolvedConfig);
|
|
1894
|
+
if (!normalisedConfig.skipValidation) diagnostics = diagnostics.concat(collectUngatedCrudDiagnostics(resolvedConfig));
|
|
1823
1895
|
const narrowedConfig = resolvedConfig;
|
|
1824
1896
|
const narrowedAdapter = configWithId.adapter;
|
|
1825
1897
|
const controller = resolveOrAutoCreateController(narrowedConfig, narrowedAdapter, repository, hasCrudRoutes);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
|
-
import "./constants-
|
|
2
|
+
import "./constants-DNre0wRg.mjs";
|
|
3
3
|
import { f as createError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
-
import { i as
|
|
5
|
-
import {
|
|
6
|
-
import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-
|
|
4
|
+
import { a as selectPluginMw, c as buildPreHandlerChain, d as buildAuthMiddlewareForPermissions, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, o as buildActionPermissionMw, r as buildRouteConfig, t as tryRegisterRoute } from "./registerRoute-DGSREwPw.mjs";
|
|
5
|
+
import { i as resolvePipelineSteps, n as buildActionPipelineHandler, u as sendControllerResponse } from "./actionPermissions-C9RMiIpc.mjs";
|
|
6
|
+
import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-ajetw3Fv.mjs";
|
|
7
7
|
//#region src/core/createActionRouter.ts
|
|
8
8
|
var createActionRouter_exports = /* @__PURE__ */ __exportAll({
|
|
9
9
|
buildActionBodySchema: () => buildActionBodySchema,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
2
|
-
import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, u as buildAuthMiddleware } from "./registerRoute-
|
|
3
|
-
import { n as validateAggregations } from "./validate-
|
|
4
|
-
import { t as buildAggregationHandler } from "./buildHandler-
|
|
2
|
+
import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, u as buildAuthMiddleware } from "./registerRoute-DGSREwPw.mjs";
|
|
3
|
+
import { n as validateAggregations } from "./validate-C2mGgSXg.mjs";
|
|
4
|
+
import { t as buildAggregationHandler } from "./buildHandler-D_2ULWxZ.mjs";
|
|
5
5
|
//#region src/core/aggregation/createAggregationRouter.ts
|
|
6
6
|
/**
|
|
7
7
|
* Register one Fastify route per aggregation. No-op when the map is
|
|
@@ -39,7 +39,8 @@ function registerOne(fastify, normalized, ctx) {
|
|
|
39
39
|
const authMw = buildAuthMiddleware(fastify, config.permissions);
|
|
40
40
|
const permissionFn = config.permissions;
|
|
41
41
|
const permissionMw = async (req, _reply) => {
|
|
42
|
-
const
|
|
42
|
+
const ctx = buildPermissionContextLite(req, normalized.name);
|
|
43
|
+
const raw = await permissionFn(ctx);
|
|
43
44
|
if (!normalizePermissionGranted(raw)) {
|
|
44
45
|
const status = req.user ? 403 : 401;
|
|
45
46
|
const reason = normalizePermissionReason(raw);
|