@classytic/arc 2.9.1 → 2.10.8

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.
Files changed (132) hide show
  1. package/README.md +20 -91
  2. package/dist/{BaseController-Vu2yc56T.mjs → BaseController-DVNKvoX4.mjs} +154 -170
  3. package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-CcN2LVrc.mjs} +1 -1
  4. package/dist/actionPermissions-TUVR3uiZ.mjs +22 -0
  5. package/dist/adapters/index.d.mts +3 -3
  6. package/dist/adapters/index.mjs +2 -2
  7. package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
  8. package/dist/audit/index.d.mts +38 -3
  9. package/dist/audit/index.mjs +54 -22
  10. package/dist/auth/index.d.mts +2 -2
  11. package/dist/auth/index.mjs +3 -3
  12. package/dist/cache/index.d.mts +17 -15
  13. package/dist/cache/index.mjs +16 -15
  14. package/dist/{caching-CjybdRwx.mjs → caching-3h93rkJM.mjs} +8 -3
  15. package/dist/cli/commands/describe.mjs +1 -1
  16. package/dist/cli/commands/docs.mjs +2 -2
  17. package/dist/cli/commands/init.mjs +1 -1
  18. package/dist/cli/commands/introspect.mjs +1 -1
  19. package/dist/context/index.d.mts +58 -0
  20. package/dist/context/index.mjs +2 -0
  21. package/dist/core/index.d.mts +2 -2
  22. package/dist/core/index.mjs +3 -4
  23. package/dist/{defineResource-C__jkwvs.mjs → core-3MWJosCH.mjs} +174 -94
  24. package/dist/{createActionRouter-DH1YFL9m.mjs → createActionRouter-C8UUB3Px.mjs} +1 -1
  25. package/dist/{createApp-CBJUJKGP.mjs → createApp-BwnEAO2h.mjs} +53 -19
  26. package/dist/docs/index.d.mts +1 -1
  27. package/dist/docs/index.mjs +2 -2
  28. package/dist/{elevation-DxQ6ACbt.mjs → elevation-Dci0AYLT.mjs} +2 -2
  29. package/dist/errorHandler-2ii4RIYr.d.mts +114 -0
  30. package/dist/{errorHandler-CZDW4EXS.mjs → errorHandler-CSxe7KIM.mjs} +1 -1
  31. package/dist/{eventPlugin-Dl7MoVWH.mjs → eventPlugin-ByU4Cv0e.mjs} +1 -1
  32. package/dist/{eventPlugin-BxvaCIZF.d.mts → eventPlugin-D1ThQ1Pp.d.mts} +1 -1
  33. package/dist/events/index.d.mts +8 -5
  34. package/dist/events/index.mjs +87 -52
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis.d.mts +1 -1
  37. package/dist/factory/index.d.mts +1 -1
  38. package/dist/factory/index.mjs +1 -1
  39. package/dist/{types-DZi1aYhm.d.mts → fields-C8Y0XLAu.d.mts} +122 -2
  40. package/dist/hooks/index.d.mts +1 -1
  41. package/dist/idempotency/index.d.mts +5 -2
  42. package/dist/idempotency/index.mjs +46 -37
  43. package/dist/{interface-YrWsmKqE.d.mts → index-BGbpGVyM.d.mts} +2107 -2756
  44. package/dist/{index-CtGKT0lf.d.mts → index-BziRPS4H.d.mts} +81 -7
  45. package/dist/{index-C-xjcA6F.d.mts → index-C_Noptz-.d.mts} +284 -409
  46. package/dist/{index-Cibkchnx.d.mts → index-EqQN6p0W.d.mts} +3 -3
  47. package/dist/index.d.mts +6 -219
  48. package/dist/index.mjs +10 -131
  49. package/dist/integrations/event-gateway.d.mts +1 -1
  50. package/dist/integrations/event-gateway.mjs +1 -1
  51. package/dist/integrations/index.d.mts +1 -1
  52. package/dist/integrations/mcp/index.d.mts +2 -2
  53. package/dist/integrations/mcp/index.mjs +1 -1
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/interface-yhyb_pLY.d.mts +77 -0
  57. package/dist/logger/index.d.mts +81 -0
  58. package/dist/{logger-CDjpjySd.mjs → logger/index.mjs} +1 -6
  59. package/dist/{memory-BFAYkf8H.mjs → memory-DqI-449b.mjs} +23 -8
  60. package/dist/middleware/index.d.mts +109 -0
  61. package/dist/middleware/index.mjs +70 -0
  62. package/dist/multipartBody-CUQGVlM_.mjs +123 -0
  63. package/dist/{openapi-CXuTG1M9.mjs → openapi-DpNpqBmo.mjs} +9 -7
  64. package/dist/org/index.d.mts +2 -2
  65. package/dist/permissions/index.d.mts +3 -4
  66. package/dist/permissions/index.mjs +5 -5
  67. package/dist/{permissions-oNZawnkR.mjs → permissions-wkqRwicB.mjs} +315 -397
  68. package/dist/pipe-CGJxqDGx.mjs +62 -0
  69. package/dist/pipeline/index.d.mts +62 -0
  70. package/dist/pipeline/index.mjs +53 -0
  71. package/dist/plugins/index.d.mts +23 -3
  72. package/dist/plugins/index.mjs +9 -11
  73. package/dist/plugins/response-cache.mjs +1 -1
  74. package/dist/plugins/tracing-entry.mjs +1 -1
  75. package/dist/presets/filesUpload.d.mts +3 -3
  76. package/dist/presets/filesUpload.mjs +255 -1
  77. package/dist/presets/index.d.mts +1 -1
  78. package/dist/presets/index.mjs +2 -2
  79. package/dist/presets/multiTenant.d.mts +1 -1
  80. package/dist/presets/multiTenant.mjs +43 -9
  81. package/dist/presets/search.d.mts +91 -4
  82. package/dist/presets/search.mjs +1 -1
  83. package/dist/{presets-hM4WhNWY.mjs → presets-CrwOvuXI.mjs} +1 -1
  84. package/dist/{queryCachePlugin-DbUVroUG.mjs → queryCachePlugin-ChLNZvFT.mjs} +9 -9
  85. package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-Dumka73q.d.mts} +1 -1
  86. package/dist/{queryParser-Cs-6SHQK.mjs → queryParser-NR__Qiju.mjs} +69 -2
  87. package/dist/{redis-stream-Bz-4q96t.d.mts → redis-stream-bkO88VHx.d.mts} +1 -1
  88. package/dist/registry/index.d.mts +1 -1
  89. package/dist/registry/index.mjs +1 -1
  90. package/dist/{requestContext-DYtmNpm5.mjs → requestContext-C38GskNt.mjs} +1 -1
  91. package/dist/{resourceToTools-C3cWymnW.mjs → resourceToTools-BhF3JV5p.mjs} +8 -3
  92. package/dist/scope/index.d.mts +2 -2
  93. package/dist/scope/index.mjs +2 -2
  94. package/dist/{sse-CJpt7LGI.mjs → sse-D8UeDwis.mjs} +1 -1
  95. package/dist/{store-helpers-DFiZl5TL.mjs → store-helpers-DYYUQbQN.mjs} +4 -0
  96. package/dist/testing/index.d.mts +6 -5
  97. package/dist/testing/index.mjs +17 -10
  98. package/dist/types/index.d.mts +5 -5
  99. package/dist/types/index.mjs +1 -31
  100. package/dist/types-CDnTEpga.mjs +27 -0
  101. package/dist/{types-CoSzA-s-.d.mts → types-CVKBssX5.d.mts} +1 -1
  102. package/dist/{types-CunEX4UX.d.mts → types-CVdgPXBW.d.mts} +20 -7
  103. package/dist/utils/index.d.mts +277 -3
  104. package/dist/utils/index.mjs +4 -5
  105. package/dist/{utils-B7FuRr9w.mjs → utils-LMwVidKy.mjs} +303 -2
  106. package/dist/{versioning-Cm8qoFDg.mjs → versioning-B6mimogM.mjs} +3 -5
  107. package/dist/versioning-CeUXHfjw.d.mts +117 -0
  108. package/package.json +31 -18
  109. package/skills/arc/SKILL.md +8 -12
  110. package/skills/arc/references/production.md +0 -41
  111. package/dist/circuitBreaker-CvXkjfrW.d.mts +0 -206
  112. package/dist/circuitBreaker-l18oRgL5.mjs +0 -284
  113. package/dist/core-DNncu0xF.mjs +0 -34
  114. package/dist/dynamic/index.d.mts +0 -93
  115. package/dist/dynamic/index.mjs +0 -122
  116. package/dist/errorHandler-DixGcttC.d.mts +0 -218
  117. package/dist/fields-BC7zcmI9.d.mts +0 -121
  118. package/dist/filesUpload-q8oHt--L.mjs +0 -377
  119. package/dist/interface-DplgQO2e.d.mts +0 -54
  120. package/dist/policies/index.d.mts +0 -425
  121. package/dist/policies/index.mjs +0 -318
  122. package/dist/rpc/index.d.mts +0 -90
  123. package/dist/rpc/index.mjs +0 -248
  124. /package/dist/{EventTransport-CqZ8FyM_.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
  125. /package/dist/{applyPermissionResult-bqGpo9ML.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
  126. /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
  127. /package/dist/{elevation-B6S5csVA.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  128. /package/dist/{errors-CqWnSqM-.mjs → errors-BqdUDja_.mjs} +0 -0
  129. /package/dist/{fields-CU6FlaDV.mjs → fields-CTMWOUDt.mjs} +0 -0
  130. /package/dist/{keys-qcD-TVJl.mjs → keys-nWQGUTu1.mjs} +0 -0
  131. /package/dist/{types-ZUu_h0jp.mjs → types-D57iXYb8.mjs} +0 -0
  132. /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Database-agnostic resource framework for Fastify. Define resources, get CRUD routes, permissions, presets, caching, events, OpenAPI, and MCP tools — without boilerplate.
4
4
 
5
- **v2.9** | Fastify 5+ | Node.js 22+ | ESM only
5
+ **v2.10** | Fastify 5+ | Node.js 22+ | ESM only
6
6
 
7
7
  ## Install
8
8
 
@@ -65,12 +65,14 @@ const app = await createApp({
65
65
  Clean DX without growing exclude lists:
66
66
 
67
67
  ```typescript
68
- import { Repository } from '@classytic/mongokit';
68
+ import { Repository, methodRegistryPlugin, batchOperationsPlugin } from '@classytic/mongokit';
69
69
 
70
70
  // app.ts — pass any RepositoryLike (mongokit / prismakit / custom)
71
71
  await fastify.register(auditPlugin, {
72
72
  autoAudit: { perResource: true },
73
- repository: new Repository(AuditModel), // or omit for in-memory dev
73
+ // batchOperationsPlugin enables deleteMany, required for purgeOlderThan()
74
+ repository: new Repository(AuditModel, [methodRegistryPlugin(), batchOperationsPlugin()]),
75
+ // or omit `repository` for in-memory dev
74
76
  });
75
77
 
76
78
  // order.resource.ts — opt in
@@ -600,7 +602,7 @@ await app.register(eventGatewayPlugin, {
600
602
  Functional composition for cross-cutting concerns:
601
603
 
602
604
  ```typescript
603
- import { pipe, guard, transform, intercept } from '@classytic/arc';
605
+ import { pipe, guard, transform, intercept } from '@classytic/arc/pipeline';
604
606
 
605
607
  const isActive = guard('isActive', (ctx) => ctx.query?.filters?.isActive !== false);
606
608
  const slugify = transform('slugify', (ctx) => ({ ...ctx, body: { ...ctx.body, slug: toSlug(ctx.body.name) } }));
@@ -737,7 +739,6 @@ Arc sets `"sideEffects": false` in [package.json](package.json), so modern bundl
737
739
  | `@classytic/arc/presets` | Preset functions + interfaces |
738
740
  | `@classytic/arc/audit` | Audit trail |
739
741
  | `@classytic/arc/idempotency` | Idempotency |
740
- | `@classytic/arc/policies` | Policy engine |
741
742
  | `@classytic/arc/schemas` | TypeBox helpers |
742
743
  | `@classytic/arc/utils` | Errors, circuit breaker, state machine, query parser |
743
744
  | `@classytic/arc/testing` | Test utilities, mocks, in-memory DB |
@@ -750,101 +751,29 @@ Arc sets `"sideEffects": false` in [package.json](package.json), so modern bundl
750
751
  | `@classytic/arc/docs` | OpenAPI generation |
751
752
  | `@classytic/arc/cli` | CLI commands (programmatic) |
752
753
 
753
- ## v2.9.1 Highlights
754
+ ## Type imports
754
755
 
755
- `auditPlugin`, `idempotencyPlugin`, and `EventOutbox` take a
756
- `repository: RepositoryLike` option — pass any `Repository` (mongokit /
757
- prismakit / your own kit). Arc calls `create` / `getOne` / `findAll` /
758
- `deleteMany` / `findOneAndUpdate` on it directly:
756
+ Arc owns framework types (`IController`, `IRequestContext`, `ResourceConfig`, `RepositoryLike`, `PaginationResult`). The repository contract lives in `@classytic/repo-core` — import those types directly:
759
757
 
760
758
  ```typescript
761
- import { Repository } from '@classytic/mongokit';
762
- import { auditPlugin } from '@classytic/arc/audit';
763
- import { idempotencyPlugin } from '@classytic/arc/idempotency';
764
- import { EventOutbox } from '@classytic/arc/events';
765
-
766
- await fastify.register(auditPlugin, { repository: new Repository(AuditModel) });
767
- await fastify.register(idempotencyPlugin, {
768
- repository: new Repository(IdempotencyModel, [
769
- methodRegistryPlugin(), batchOperationsPlugin(), mongoOperationsPlugin(),
770
- ]),
771
- });
772
- new EventOutbox({ repository: new Repository(OutboxModel), transport });
773
- ```
774
-
775
- Memory + Redis stores unchanged via `store` / `customStores`. Requires
776
- `@classytic/mongokit ≥3.8.0`.
777
-
778
- ## v2.9 Highlights
779
-
780
- **Security defaults (breaking):**
781
- - **Field-write perms reject by default** — requests with non-writable fields return 403 with denied-field list. Opt into legacy silent-strip via `defineResource({ onFieldWriteDenied: 'strip' })`.
782
- - **multiTenant preset injects org on UPDATE** — body-supplied `organizationId` is overwritten with caller's scope (prior: CREATE only; a member could hop their own doc to another tenant).
783
- - **Elevation always emits `arc.scope.elevated`** — privilege elevation can no longer be silently un-audited. Subscribe via `fastify.events.subscribe('arc.scope.elevated', …)`. `onElevation` callback still supported.
784
- - **`verifySignature` throws on parsed body** — prevents the common `req.body` vs `req.rawBody` footgun that looks like a wrong secret.
785
- - **Upload `sanitizeFilename` policy** — rejects path separators, NUL, `.`/`..`, >255 chars by default. Flexible: pass `false` / `'*'` / custom function to override.
786
- - **Idempotency `namespace`** — optional key folded into fingerprint for shared-store deployments (prod + canary on one Redis).
759
+ // Arc framework types
760
+ import type { IRequestContext, RepositoryLike, PaginationResult } from '@classytic/arc';
787
761
 
788
- **Event contract v2 (additive):**
789
- - `EventMeta` gets `schemaVersion`, `causationId`, `partitionKey`, `source`, `idempotencyKey`, `aggregate: { type, id }`
790
- - `createChildEvent(parent, type, payload)` auto-chains causation + inherits correlation / userId / organizationId / source / idempotencyKey (aggregate stays explicit)
791
- - `DeadLetteredEvent<T>` type + optional `transport.deadLetter()` for first-class DLQ
792
- - `withRetry({ transport })` auto-routes exhausted events to `transport.deadLetter()` — no custom `$deadLetter` plumbing
793
- - Downstream packages narrow `aggregate.type` to a closed DDD union via interface extension
794
-
795
- **Outbox v2 (additive):**
796
- - `EventOutbox.store()` auto-maps `event.meta.idempotencyKey` → `OutboxWriteOptions.dedupeKey` (caller's explicit `dedupeKey` still wins)
797
- - `new EventOutbox({ failurePolicy: ({ attempts, error }) => ({ retryAt?, deadLetter? }) })` — centralises retry + DLQ escalation, no more hand-rolled `exponentialBackoff` at every failure site
798
- - `outbox.getDeadLettered(limit)` returns typed `DeadLetteredEvent[]` — same shape `withRetry` produces, closes the loop between retry DLQ and outbox DLQ state
799
- - `RelayResult.deadLettered` — per-batch DLQ transition count for dashboards
800
- - Durable outbox via any `RepositoryLike` — `new EventOutbox({ repository, transport })` (2.9.1); multi-worker claim, TTL purge, dedupe, and session-threaded writes come from the repository's backing kit
801
-
802
- **Removed (no replacement kept):**
803
- - `createActionRouter`, `buildActionBodySchema` — use `defineResource({ actions })`
804
- - `ResourceConfig.onRegister` — use `actions` or resource `hooks`
805
- - `PluginResourceResult.additionalRoutes` → `routes: RouteDefinition[]`
806
- - `PolicyContext` / `PolicyResult` `any` fields → `unknown` (tighten downstream narrowing)
807
-
808
- ## v2.8.4 Highlights
809
-
810
- - **MCP ↔ AI SDK bridge** — expose AI SDK `tool()` definitions over MCP without duplicating code. `bridgeToMcp(bridge)` adapts any AI SDK tool into an MCP tool with automatic auth, guard delegation, and `{ error } → isError` envelope translation. `buildMcpToolsFromBridges(bridges, { include, exclude })` registers a whole catalog at once with per-environment filtering.
811
-
812
- ```typescript
813
- import { bridgeToMcp, buildMcpToolsFromBridges, type McpBridge } from '@classytic/arc/mcp';
814
-
815
- export const triggerJobBridge: McpBridge = {
816
- name: 'trigger_job',
817
- description: 'Start a job.',
818
- inputSchema: { phase: z.enum(['investigate', 'fix']) },
819
- annotations: { destructiveHint: true },
820
- buildTool: (ctx) => buildTriggerJobTool(getUserId(ctx) ?? ''),
821
- };
822
-
823
- await app.register(mcpPlugin, {
824
- resources,
825
- extraTools: buildMcpToolsFromBridges([triggerJobBridge]),
826
- });
762
+ // Repository contract (repo-core is the single source of truth)
763
+ import type { StandardRepo, WriteOptions, QueryOptions } from '@classytic/repo-core/repository';
764
+ import type { OffsetPaginationResult } from '@classytic/repo-core/pagination';
827
765
  ```
828
766
 
829
- ## v2.8.1 Highlights
767
+ > Arc 2.10 dropped the legacy `CrudRepository`, `PaginatedResult`, and pass-through `WriteOptions`/`QueryOptions` re-exports. See [CHANGELOG.md](CHANGELOG.md#210) for the migration table.
830
768
 
831
- - **Per-action discriminated validation** — `actions` schemas now enforce required fields via a `oneOf` body schema; missing inputs are rejected at the HTTP layer by AJV (no more silent bypass)
832
- - **Actions in OpenAPI** — `POST /:id/action` endpoint auto-generated from `ResourceDefinition.actions`, with per-action descriptions and the same discriminated body schema as the runtime router
833
- - **Route/action metadata preserved** — `mcp: false`, `description`, `annotations` no longer dropped during `routes → additionalRoutes` normalization
834
- - **Canonical source retained** — `ResourceDefinition.routes` and `ResourceDefinition.actions` now kept as declared, so OpenAPI/MCP/registry can read the original shape
835
- - **Outbox hardening** — expanded `OutboxStore` contract (`claimPending`, `fail`, write options, dedupe, visibleAt), ownership-mismatch throws, onError reporting, safe multi-worker relay
836
- - **`slugLookup` fallback** — works with MongoKit's default Repository (no custom `getBySlug` needed)
769
+ ## v2.10 Highlights
837
770
 
838
- ## v2.8.0 Highlights
771
+ - **Clean-break on repo-core types** — `CrudRepository` / `PaginatedResult` / pass-through repo options removed from arc's public surface. Import `StandardRepo`, `OffsetPaginationResult`, etc. directly from `@classytic/repo-core`. See [CHANGELOG.md](CHANGELOG.md) for the rewrite table.
772
+ - **Outbox bugfix** — `repositoryAsOutboxStore.fail()` now passes `updatePipeline: true` to `findOneAndUpdate`, so retry / DLQ transitions work on mongokit ≥3.10.
773
+ - **Plugin requirements documented** — audit / outbox / idempotency require mongokit's `methodRegistryPlugin` + `batchOperationsPlugin` for `deleteMany`; README snippets + production-ops docs now show the correct chain.
774
+ - **Removed** — `@classytic/arc/policies` (use `permissions/`), `@classytic/arc/rpc`, `@classytic/arc/dynamic` (use `factory/loadResources`).
839
775
 
840
- - **MCP Integration** expose resources as AI agent tools (stateless by default, service scope, multi-tenancy)
841
- - **Reply Helpers** — `reply.ok()`, `reply.fail()`, `reply.paginated()`, `reply.stream()` (opt-in)
842
- - **Error Mappers** — class-based `instanceof` domain error → HTTP response mapping
843
- - **Multipart Body** — `multipartBody()` middleware for file upload in CRUD routes
844
- - **Service Scope** — `kind: "service"` RequestScope for machine-to-machine auth (MCP + WebSocket)
845
- - **BigInt Serialization** — `serializeBigInt: true` auto-converts BigInt → Number
846
- - **Event WAL** — skips internal `arc.*` events to prevent startup timeout with durable stores
847
- - **Security** — `auth: false` produces null `ctx.user` (prevents anonymous bypass of `!!ctx.user` guards)
776
+ See [CHANGELOG.md](CHANGELOG.md) for the full v2.9 / v2.8 / v2.7 history.
848
777
 
849
778
  ## License
850
779
 
@@ -1,33 +1,26 @@
1
- import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-Cxde4rpC.mjs";
1
+ import { h as SYSTEM_FIELDS, o as DEFAULT_TENANT_FIELD } from "./constants-BhY1OHoH.mjs";
2
+ import { arcLog } from "./logger/index.mjs";
2
3
  import { _ as isElevated, n as PUBLIC_SCOPE, o as getOrgId, v as isMember } from "./types-AOD8fxIw.mjs";
3
- import { t as buildQueryKey } from "./keys-qcD-TVJl.mjs";
4
- import { getUserId } from "./types/index.mjs";
5
- import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-CU6FlaDV.mjs";
6
- import { t as getUserRoles } from "./types-ZUu_h0jp.mjs";
7
- import { r as ForbiddenError } from "./errors-CqWnSqM-.mjs";
8
- import { t as ArcQueryParser } from "./queryParser-Cs-6SHQK.mjs";
4
+ import { r as simpleEqualityMatcher, t as ArcQueryParser } from "./queryParser-NR__Qiju.mjs";
5
+ import { t as buildQueryKey } from "./keys-nWQGUTu1.mjs";
6
+ import { n as getUserId } from "./types-CDnTEpga.mjs";
7
+ import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-CTMWOUDt.mjs";
8
+ import { t as getUserRoles } from "./types-D57iXYb8.mjs";
9
+ import { r as ForbiddenError } from "./errors-BqdUDja_.mjs";
9
10
  //#region src/core/AccessControl.ts
10
- /**
11
- * AccessControl - Composable access control logic extracted from BaseController.
12
- *
13
- * Handles ID filtering, policy filter checking, org/tenant scope validation,
14
- * ownership verification, and fetch-with-access-control patterns.
15
- *
16
- * Designed to be used standalone or composed into controllers.
17
- */
18
- var AccessControl = class AccessControl {
11
+ const log = arcLog("access-control");
12
+ var AccessControl = class {
19
13
  tenantField;
20
14
  idField;
21
15
  _adapterMatchesFilter;
22
- /** Patterns that indicate dangerous regex (nested quantifiers, excessive backtracking).
23
- * Uses [^...] character classes instead of .+ to avoid backtracking in the detector itself. */
24
- static DANGEROUS_REGEX = /(\{[0-9]+,\}[^{]*\{[0-9]+,\})|(\+[^+]*\+)|(\*[^*]*\*)|(\.\*){3,}|\\1/;
25
- /** Forbidden paths that could lead to prototype pollution */
26
- static FORBIDDEN_PATHS = [
27
- "__proto__",
28
- "constructor",
29
- "prototype"
30
- ];
16
+ /**
17
+ * One-shot latch for the "adapter didn't supply matchesFilter, in-memory
18
+ * policy-filter re-check is skipped" warning. The primary fetch path
19
+ * (`getOne(compoundFilter)`) already applied filters at the DB layer;
20
+ * this warn only fires when `validateItemAccess` runs and the adapter
21
+ * hasn't provided a native matcher for the post-hoc re-check.
22
+ */
23
+ _warnedNoMatcher = false;
31
24
  constructor(config) {
32
25
  this.tenantField = config.tenantField;
33
26
  this.idField = config.idField;
@@ -48,17 +41,54 @@ var AccessControl = class AccessControl {
48
41
  return filter;
49
42
  }
50
43
  /**
51
- * Check if item matches policy filters (for get/update/delete operations)
52
- * Validates that fetched item satisfies all policy constraints
44
+ * Check if a post-fetch item matches the request's `_policyFilters`.
45
+ *
46
+ * **When this runs:** only on paths where the primary fetch path did NOT
47
+ * apply policy filters at the DB layer — notably `validateItemAccess`
48
+ * (used by `getBySlug` and cache revalidation). The main `fetchDetailed`
49
+ * path builds a compound filter (`buildIdFilter`) and passes it to
50
+ * `repository.getOne(compoundFilter)`, so the DB has already enforced
51
+ * the filter and an in-memory re-check would be redundant.
52
+ *
53
+ * **Evaluation order (fail-closed):**
54
+ * 1. No `_policyFilters` set → `true` (nothing to enforce).
55
+ * 2. Adapter supplied `matchesFilter` → delegate to it verbatim. Adapters
56
+ * are expected to handle every filter shape the host emits
57
+ * (mongokit/sqlitekit evaluate at the DB layer; Prisma/custom engines
58
+ * can wrap their own predicate engine).
59
+ * 3. No adapter matcher → fall back to `simpleEqualityMatcher` — arc's
60
+ * built-in flat-key equality helper. This is defense-in-depth for the
61
+ * common case: arc's own permission helpers emit flat filters
62
+ * (`{userId: …}`, `{organizationId: …}`), which this matcher evaluates
63
+ * correctly. Operator-shaped filters (`$in`, `$ne`, `$regex`, `$and`,
64
+ * `$or`) are **rejected** (the matcher returns `false`) — fail-closed
65
+ * rather than fail-open. A one-shot warn flags the gap so adapter
66
+ * authors can wire a richer matcher.
53
67
  *
54
- * Delegates to adapter-provided matchesFilter if available (for SQL, etc.),
55
- * otherwise falls back to built-in MongoDB-style matching.
68
+ * Arc deliberately does NOT ship a full MongoDB-syntax matcher:
69
+ * re-implementing Mongo in JS was dead code for mongokit users (the DB
70
+ * did it) and silently wrong for non-Mongo adapters. The flat-equality
71
+ * fallback is small (~20 LOC), correct in both dialects, and closes the
72
+ * previous `getBySlug`-style policy-bypass path.
56
73
  */
57
74
  checkPolicyFilters(item, req) {
58
75
  const policyFilters = this._meta(req)?._policyFilters;
59
- if (!policyFilters) return true;
76
+ if (!policyFilters || Object.keys(policyFilters).length === 0) return true;
60
77
  if (this._adapterMatchesFilter) return this._adapterMatchesFilter(item, policyFilters);
61
- return this.defaultMatchesPolicyFilters(item, policyFilters);
78
+ if (Object.values(policyFilters).some((v) => v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype && Object.keys(v).some((k) => k.startsWith("$")))) this._warnNoMatcher(policyFilters);
79
+ return simpleEqualityMatcher(item, policyFilters);
80
+ }
81
+ /**
82
+ * Emit a one-shot warn when policy filters contain operators (`$in`,
83
+ * `$ne`, `$regex`, etc.) and no `DataAdapter.matchesFilter` is wired —
84
+ * arc's flat-equality fallback fail-closes on operators, so the host
85
+ * sees 404s on docs that should match. Latched on `_warnedNoMatcher`
86
+ * so subsequent requests stay quiet.
87
+ */
88
+ _warnNoMatcher(policyFilters) {
89
+ if (this._warnedNoMatcher) return;
90
+ this._warnedNoMatcher = true;
91
+ log.warn("`_policyFilters` contains operator-shaped entries (e.g. `$in`, `$ne`, `$regex`) but `DataAdapter.matchesFilter` is not set. Arc's flat-equality fallback cannot evaluate operators and will reject these items on non-compound fetches (`validateItemAccess`, `getBySlug`, cache revalidation). Wire up `matchesFilter` on your adapter — use `matchFilter` from `@classytic/repo-core/filter` for IR-based adapters, or your DB's native predicate engine.", { policyFilterKeys: Object.keys(policyFilters) });
62
92
  }
63
93
  /**
64
94
  * Check org/tenant scope for a document — uses configurable tenantField.
@@ -72,7 +102,6 @@ var AccessControl = class AccessControl {
72
102
  const scope = arcContext?._scope;
73
103
  const orgId = scope ? getOrgId(scope) : void 0;
74
104
  if (!item || !orgId) return true;
75
- if (scope && isElevated(scope) && !orgId) return true;
76
105
  const itemOrgId = item[this.tenantField];
77
106
  if (!itemOrgId) return false;
78
107
  return String(itemOrgId) === String(orgId);
@@ -130,7 +159,25 @@ var AccessControl = class AccessControl {
130
159
  };
131
160
  if (hasCompoundFilters) {
132
161
  const idOnly = { [this.idField]: id };
133
- const rawDoc = await repository.getOne(idOnly);
162
+ const rawGetOne = repository.getOne.bind(repository);
163
+ let rawDoc = null;
164
+ try {
165
+ rawDoc = await rawGetOne(idOnly);
166
+ } catch (unscopedErr) {
167
+ if (translateStatus404(unscopedErr)) return {
168
+ doc: null,
169
+ reason: "NOT_FOUND"
170
+ };
171
+ try {
172
+ rawDoc = await rawGetOne(idOnly, queryOptions);
173
+ } catch (scopedErr) {
174
+ if (translateStatus404(scopedErr)) return {
175
+ doc: null,
176
+ reason: "NOT_FOUND"
177
+ };
178
+ throw scopedErr;
179
+ }
180
+ }
134
181
  if (rawDoc) {
135
182
  const arcContext = this._meta(req);
136
183
  if (!this.checkOrgScope(rawDoc, arcContext)) return {
@@ -191,101 +238,6 @@ var AccessControl = class AccessControl {
191
238
  _meta(req) {
192
239
  return req.metadata;
193
240
  }
194
- /**
195
- * Check if a value matches a MongoDB query operator
196
- */
197
- matchesOperator(itemValue, operator, filterValue) {
198
- const equalsByValue = (a, b) => String(a) === String(b);
199
- switch (operator) {
200
- case "$eq": return equalsByValue(itemValue, filterValue);
201
- case "$ne": return !equalsByValue(itemValue, filterValue);
202
- case "$gt": return typeof itemValue === "number" && typeof filterValue === "number" && itemValue > filterValue;
203
- case "$gte": return typeof itemValue === "number" && typeof filterValue === "number" && itemValue >= filterValue;
204
- case "$lt": return typeof itemValue === "number" && typeof filterValue === "number" && itemValue < filterValue;
205
- case "$lte": return typeof itemValue === "number" && typeof filterValue === "number" && itemValue <= filterValue;
206
- case "$in":
207
- if (!Array.isArray(filterValue)) return false;
208
- if (Array.isArray(itemValue)) return itemValue.some((v) => filterValue.some((fv) => equalsByValue(v, fv)));
209
- return filterValue.some((fv) => equalsByValue(itemValue, fv));
210
- case "$nin":
211
- if (!Array.isArray(filterValue)) return false;
212
- if (Array.isArray(itemValue)) return itemValue.every((v) => filterValue.every((fv) => !equalsByValue(v, fv)));
213
- return filterValue.every((fv) => !equalsByValue(itemValue, fv));
214
- case "$exists": return filterValue ? itemValue !== void 0 : itemValue === void 0;
215
- case "$regex":
216
- if (typeof itemValue === "string" && (typeof filterValue === "string" || filterValue instanceof RegExp)) return (typeof filterValue === "string" ? AccessControl.safeRegex(filterValue) : filterValue)?.test(itemValue) ?? false;
217
- return false;
218
- default: return false;
219
- }
220
- }
221
- /**
222
- * Check if item matches a single filter condition
223
- * Supports nested paths (e.g., "owner.id", "metadata.status")
224
- */
225
- matchesFilter(item, key, filterValue) {
226
- const itemValue = key.includes(".") ? this.getNestedValue(item, key) : item[key];
227
- if (filterValue && typeof filterValue === "object" && !Array.isArray(filterValue)) {
228
- if (Object.keys(filterValue).some((op) => op.startsWith("$"))) {
229
- for (const [operator, opValue] of Object.entries(filterValue)) if (!this.matchesOperator(itemValue, operator, opValue)) return false;
230
- return true;
231
- }
232
- }
233
- if (Array.isArray(itemValue)) return itemValue.some((v) => String(v) === String(filterValue));
234
- return String(itemValue) === String(filterValue);
235
- }
236
- /**
237
- * Built-in MongoDB-style policy filter matching.
238
- * Supports: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $regex, $and, $or
239
- */
240
- defaultMatchesPolicyFilters(item, policyFilters) {
241
- if (policyFilters.$and && Array.isArray(policyFilters.$and)) {
242
- if (!policyFilters.$and.every((condition) => {
243
- return Object.entries(condition).every(([key, value]) => {
244
- return this.matchesFilter(item, key, value);
245
- });
246
- })) return false;
247
- }
248
- if (policyFilters.$or && Array.isArray(policyFilters.$or)) {
249
- if (!policyFilters.$or.some((condition) => {
250
- return Object.entries(condition).every(([key, value]) => {
251
- return this.matchesFilter(item, key, value);
252
- });
253
- })) return false;
254
- }
255
- for (const [key, value] of Object.entries(policyFilters)) {
256
- if (key.startsWith("$")) continue;
257
- if (!this.matchesFilter(item, key, value)) return false;
258
- }
259
- return true;
260
- }
261
- /**
262
- * Get nested value from object using dot notation (e.g., "owner.id")
263
- * Security: Validates path against forbidden patterns to prevent prototype pollution
264
- */
265
- getNestedValue(obj, path) {
266
- if (AccessControl.FORBIDDEN_PATHS.some((p) => path.toLowerCase().includes(p))) return;
267
- const keys = path.split(".");
268
- let value = obj;
269
- for (const key of keys) {
270
- if (value == null) return void 0;
271
- if (AccessControl.FORBIDDEN_PATHS.includes(key.toLowerCase())) return;
272
- value = value[key];
273
- }
274
- return value;
275
- }
276
- /**
277
- * Create a safe RegExp from a string, guarding against ReDoS.
278
- * Returns null if the pattern is invalid or dangerous.
279
- */
280
- static safeRegex(pattern) {
281
- if (pattern.length > 200) return null;
282
- if (AccessControl.DANGEROUS_REGEX.test(pattern)) return null;
283
- try {
284
- return new RegExp(pattern);
285
- } catch {
286
- return null;
287
- }
288
- }
289
241
  };
290
242
  var BodySanitizer = class {
291
243
  schemaOptions;
@@ -304,10 +256,13 @@ var BodySanitizer = class {
304
256
  sanitize(body, _operation, req, meta) {
305
257
  let sanitized = { ...body };
306
258
  for (const field of SYSTEM_FIELDS) delete sanitized[field];
259
+ const scopeForRules = req ? (meta ?? req.metadata)?._scope ?? PUBLIC_SCOPE : void 0;
260
+ const scopeIsElevated = scopeForRules ? isElevated(scopeForRules) : false;
307
261
  const fieldRules = this.schemaOptions.fieldRules ?? {};
308
262
  for (const [field, rules] of Object.entries(fieldRules)) {
309
- if (rules.systemManaged || rules.readonly) delete sanitized[field];
310
- if (_operation === "update" && (rules.immutable || rules.immutableAfterCreate)) delete sanitized[field];
263
+ const bypass = Boolean(rules.preserveForElevated) && scopeIsElevated;
264
+ if ((rules.systemManaged || rules.readonly) && !bypass) delete sanitized[field];
265
+ if (_operation === "update" && (rules.immutable || rules.immutableAfterCreate) && !bypass) delete sanitized[field];
311
266
  }
312
267
  if (req) {
313
268
  const arcContext = meta ?? req.metadata;
@@ -344,6 +299,7 @@ var QueryResolver = class {
344
299
  queryParser;
345
300
  maxLimit;
346
301
  defaultLimit;
302
+ /** `undefined` means "no default sort" (caller passed `false`). */
347
303
  defaultSort;
348
304
  schemaOptions;
349
305
  tenantField;
@@ -351,7 +307,7 @@ var QueryResolver = class {
351
307
  this.queryParser = config.queryParser ?? getDefaultQueryParser();
352
308
  this.maxLimit = config.maxLimit ?? 100;
353
309
  this.defaultLimit = config.defaultLimit ?? 20;
354
- this.defaultSort = config.defaultSort ?? "-createdAt";
310
+ this.defaultSort = config.defaultSort === false ? void 0 : config.defaultSort ?? "-createdAt";
355
311
  this.schemaOptions = config.schemaOptions ?? {};
356
312
  this.tenantField = config.tenantField !== void 0 ? config.tenantField : DEFAULT_TENANT_FIELD;
357
313
  }
@@ -482,6 +438,7 @@ var BaseController = class {
482
438
  queryParser;
483
439
  maxLimit;
484
440
  defaultLimit;
441
+ /** `undefined` means "no default sort" (caller passed `false`). */
485
442
  defaultSort;
486
443
  resourceName;
487
444
  tenantField;
@@ -501,7 +458,7 @@ var BaseController = class {
501
458
  this.queryParser = options.queryParser ?? getDefaultQueryParser();
502
459
  this.maxLimit = options.maxLimit ?? 100;
503
460
  this.defaultLimit = options.defaultLimit ?? 20;
504
- this.defaultSort = options.defaultSort ?? "-createdAt";
461
+ this.defaultSort = options.defaultSort === false ? void 0 : options.defaultSort ?? "-createdAt";
505
462
  this.resourceName = options.resourceName;
506
463
  this.tenantField = options.tenantField !== void 0 ? options.tenantField : DEFAULT_TENANT_FIELD;
507
464
  this.idField = options.idField ?? repository?.idField ?? "_id";
@@ -521,7 +478,7 @@ var BaseController = class {
521
478
  queryParser: this.queryParser,
522
479
  maxLimit: this.maxLimit,
523
480
  defaultLimit: this.defaultLimit,
524
- defaultSort: this.defaultSort,
481
+ defaultSort: options.defaultSort,
525
482
  schemaOptions: this.schemaOptions,
526
483
  tenantField: this.tenantField
527
484
  });
@@ -541,6 +498,48 @@ var BaseController = class {
541
498
  getTenantField() {
542
499
  return this.tenantField || void 0;
543
500
  }
501
+ /**
502
+ * Build top-level tenant options to thread into the repository call.
503
+ *
504
+ * **Why this exists:** repo plugins (e.g. `@classytic/mongokit`'s
505
+ * `multiTenantPlugin`) read tenant scope from the TOP of the repository
506
+ * operation context — `context.organizationId`, not `context.data.organizationId`
507
+ * or `context.context.organizationId`. Without this stamping, a tenant-scoped
508
+ * repository throws `Missing 'organizationId' in context for '<op>'` even
509
+ * though arc already injected the tenant into the request body.
510
+ *
511
+ * **What this returns:**
512
+ * - `{ [tenantField]: orgId }` when the resource is tenant-scoped and the
513
+ * caller's scope carries an org ID (member, service key bound to an org,
514
+ * elevated admin impersonating an org).
515
+ * - `{}` otherwise — platform-universal resources (`tenantField: false`),
516
+ * public/anonymous reads, elevated admins without an org target.
517
+ *
518
+ * **Call sites:** every `this.repository.*` CRUD entry — `create`, `update`,
519
+ * `delete`, `getAll` (via list), plus merged into `QueryOptions` for the
520
+ * access-controlled read path (`accessControl.fetchDetailed` → `getById`/`getOne`).
521
+ *
522
+ * **Name of the field:** uses the instance's own `tenantField` configuration
523
+ * (default `organizationId`). Matches mongokit's `multiTenantPlugin` default
524
+ * `contextKey` so host apps don't need to override either side.
525
+ *
526
+ * Multi-field tenancy (via `multiTenantPreset({ tenantFields: [...] })`)
527
+ * resolves additional fields at middleware time and stashes them on
528
+ * `_tenantFields` — {@link tenantRepoOptions} merges those in too.
529
+ */
530
+ tenantRepoOptions(req) {
531
+ const out = {};
532
+ if (this.tenantField) {
533
+ const scope = this.meta(req)?._scope;
534
+ const orgId = scope ? getOrgId(scope) : void 0;
535
+ if (orgId) out[this.tenantField] = orgId;
536
+ }
537
+ const presetFields = req._tenantFields;
538
+ if (presetFields && typeof presetFields === "object") {
539
+ for (const [key, value] of Object.entries(presetFields)) if (value != null && out[key] == null) out[key] = value;
540
+ }
541
+ return out;
542
+ }
544
543
  /** Extract typed Arc internal metadata from request */
545
544
  meta(req) {
546
545
  return req.metadata;
@@ -663,21 +662,15 @@ var BaseController = class {
663
662
  /** Execute list query through hooks (extracted for cache revalidation) */
664
663
  async executeListQuery(options, req) {
665
664
  const hooks = this.getHooks(req);
666
- const repoGetAll = async () => this.repository.getAll(options);
667
- const result = hooks && this.resourceName ? await hooks.executeAround(this.resourceName, "list", options, repoGetAll, {
665
+ const getAllParams = {
666
+ ...options,
667
+ ...this.tenantRepoOptions(req)
668
+ };
669
+ const repoGetAll = async () => this.repository.getAll(getAllParams);
670
+ return hooks && this.resourceName ? await hooks.executeAround(this.resourceName, "list", options, repoGetAll, {
668
671
  user: req.user,
669
672
  context: this.meta(req)
670
673
  }) : await repoGetAll();
671
- if (Array.isArray(result)) return {
672
- docs: result,
673
- page: 1,
674
- limit: result.length,
675
- total: result.length,
676
- pages: 1,
677
- hasNext: false,
678
- hasPrev: false
679
- };
680
- return result;
681
674
  }
682
675
  async get(req) {
683
676
  const id = req.params.id;
@@ -686,7 +679,10 @@ var BaseController = class {
686
679
  error: "ID parameter is required",
687
680
  status: 400
688
681
  };
689
- const options = this.queryResolver.resolve(req, this.meta(req));
682
+ const options = {
683
+ ...this.queryResolver.resolve(req, this.meta(req)),
684
+ ...this.tenantRepoOptions(req)
685
+ };
690
686
  const cacheConfig = this.resolveCacheConfig("byId");
691
687
  const qc = req.server?.queryCache;
692
688
  if (cacheConfig && qc) {
@@ -782,7 +778,8 @@ var BaseController = class {
782
778
  }
783
779
  const repoCreate = async () => this.repository.create(processedData, {
784
780
  user,
785
- context: arcContext
781
+ context: arcContext,
782
+ ...this.tenantRepoOptions(req)
786
783
  });
787
784
  let item;
788
785
  if (hooks && this.resourceName) {
@@ -814,7 +811,7 @@ var BaseController = class {
814
811
  const user = req.user;
815
812
  const userId = getUserId(user);
816
813
  if (userId) data.updatedBy = userId;
817
- const { doc: existing, reason: updateReason } = await this.accessControl.fetchDetailed(id, req, this.repository);
814
+ const { doc: existing, reason: updateReason } = await this.accessControl.fetchDetailed(id, req, this.repository, this.tenantRepoOptions(req));
818
815
  if (!existing) return this.notFoundResponse(updateReason);
819
816
  if (!this.accessControl.checkOwnership(existing, req)) return {
820
817
  success: false,
@@ -847,7 +844,8 @@ var BaseController = class {
847
844
  }
848
845
  const repoUpdate = async () => this.repository.update(repoId, processedData, {
849
846
  user,
850
- context: arcContext
847
+ context: arcContext,
848
+ ...this.tenantRepoOptions(req)
851
849
  });
852
850
  let item;
853
851
  if (hooks && this.resourceName) {
@@ -885,7 +883,7 @@ var BaseController = class {
885
883
  };
886
884
  const arcContext = this.meta(req);
887
885
  const user = req.user;
888
- const { doc: existing, reason: deleteReason } = await this.accessControl.fetchDetailed(id, req, this.repository);
886
+ const { doc: existing, reason: deleteReason } = await this.accessControl.fetchDetailed(id, req, this.repository, this.tenantRepoOptions(req));
889
887
  if (!existing) return this.notFoundResponse(deleteReason);
890
888
  if (!this.accessControl.checkOwnership(existing, req)) return {
891
889
  success: false,
@@ -916,6 +914,7 @@ var BaseController = class {
916
914
  const repoDelete = async () => this.repository.delete(repoId, {
917
915
  user,
918
916
  context: arcContext,
917
+ ...this.tenantRepoOptions(req),
919
918
  ...deleteMode ? { mode: deleteMode } : {}
920
919
  });
921
920
  let result;
@@ -951,7 +950,10 @@ var BaseController = class {
951
950
  async getBySlug(req) {
952
951
  const slugField = this._presetFields.slugField ?? "slug";
953
952
  const slug = req.params[slugField] ?? req.params.slug;
954
- const options = this.queryResolver.resolve(req, this.meta(req));
953
+ const options = {
954
+ ...this.queryResolver.resolve(req, this.meta(req)),
955
+ ...this.tenantRepoOptions(req)
956
+ };
955
957
  const repo = this.repository;
956
958
  let item = null;
957
959
  if (repo.getBySlug) item = await repo.getBySlug(slug, options);
@@ -981,27 +983,9 @@ var BaseController = class {
981
983
  status: 501
982
984
  };
983
985
  const parsed = this.queryResolver.resolve(req, this.meta(req));
984
- const result = await repo.getDeleted(parsed, parsed);
985
- if (Array.isArray(result)) {
986
- const docs = result;
987
- return {
988
- success: true,
989
- data: {
990
- method: "offset",
991
- docs,
992
- page: 1,
993
- limit: docs.length,
994
- total: docs.length,
995
- pages: 1,
996
- hasNext: false,
997
- hasPrev: false
998
- },
999
- status: 200
1000
- };
1001
- }
1002
986
  return {
1003
987
  success: true,
1004
- data: result,
988
+ data: await repo.getDeleted(parsed, parsed),
1005
989
  status: 200
1006
990
  };
1007
991
  }
@@ -1,4 +1,4 @@
1
- import { t as CRUD_OPERATIONS } from "./constants-Cxde4rpC.mjs";
1
+ import { t as CRUD_OPERATIONS } from "./constants-BhY1OHoH.mjs";
2
2
  //#region src/registry/ResourceRegistry.ts
3
3
  /**
4
4
  * Resource Registry
@@ -0,0 +1,22 @@
1
+ //#region src/core/actionPermissions.ts
2
+ /**
3
+ * Return the effective `PermissionCheck` for a single action, or `undefined`
4
+ * when the resource declares no gate at any level.
5
+ *
6
+ * Callers decide what "no gate" means:
7
+ * - HTTP: boot-time throw in `normalizeActionsToRouterConfig`.
8
+ * - MCP: treated as allow (legacy) — but the HTTP fallback now fills the
9
+ * gap when `permissions.update` is set, so the MCP hole closes too.
10
+ * - OpenAPI: docs advertise the endpoint as unauthenticated.
11
+ */
12
+ function resolveActionPermission(input) {
13
+ const { action, resourcePermissions, resourceActionPermissions, globalAuth } = input;
14
+ const explicit = typeof action !== "function" && action.permissions ? action.permissions : void 0;
15
+ if (explicit) return explicit;
16
+ if (resourceActionPermissions) return resourceActionPermissions;
17
+ if (globalAuth) return globalAuth;
18
+ const updateFallback = resourcePermissions?.update;
19
+ if (updateFallback) return updateFallback;
20
+ }
21
+ //#endregion
22
+ export { resolveActionPermission as t };