@classytic/arc 2.30.0 → 2.32.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/LICENSE +1 -1
- package/README.md +7 -0
- package/TRADEMARK.md +22 -0
- package/dist/{BaseController-B5DwufbB.mjs → BaseController-Dk34dxhI.mjs} +21 -19
- package/dist/{EventTransport-QmY7A258.d.mts → EventTransport-2uVV34WU.d.mts} +1 -0
- package/dist/{EventTransport-C6YA8xY9.mjs → EventTransport-BV10D0oG.mjs} +1 -0
- package/dist/{HookSystem-WkIougo6.mjs → HookSystem-vimw5D7Z.mjs} +2 -1
- package/dist/{QueryCache-DOMT_iq7.d.mts → QueryCache-DFA-QrU9.d.mts} +2 -1
- package/dist/{ResourceRegistry-y9Hnypus.mjs → ResourceRegistry-BVr7PgA0.mjs} +18 -15
- package/dist/{actionPermissions-CAphcmUG.mjs → actionPermissions-Dti7ebyv.mjs} +1 -0
- package/dist/audit/index.d.mts +3 -2
- package/dist/audit/index.mjs +2 -1
- package/dist/auth/audit.d.mts +1 -0
- package/dist/auth/audit.mjs +1 -0
- package/dist/auth/index.d.mts +5 -4
- package/dist/auth/index.mjs +11 -9
- package/dist/auth/redis-session.d.mts +2 -1
- package/dist/auth/redis-session.mjs +1 -0
- package/dist/{applyPermissionResult-Cf6zUCvb.mjs → authorizationDecision-__T3EJDA.mjs} +19 -10
- package/dist/{buildHandler-BYS3Vkfz.mjs → buildHandler-Dn7rzUJo.mjs} +6 -4
- package/dist/cache/index.d.mts +4 -3
- package/dist/cache/index.mjs +4 -3
- package/dist/{caching-_NJeDd1_.mjs → caching-DFJqHyFs.mjs} +2 -1
- package/dist/cleanup/index.d.mts +39 -16
- package/dist/cleanup/index.mjs +50 -38
- package/dist/cli/commands/describe.d.mts +2 -1
- package/dist/cli/commands/describe.mjs +4 -5
- package/dist/cli/commands/docs.d.mts +1 -0
- package/dist/cli/commands/docs.mjs +3 -2
- package/dist/cli/commands/doctor.d.mts +1 -0
- package/dist/cli/commands/doctor.mjs +1 -0
- package/dist/cli/commands/generate.d.mts +2 -1
- package/dist/cli/commands/generate.mjs +2 -1
- package/dist/cli/commands/init.d.mts +2 -1
- package/dist/cli/commands/init.mjs +2 -1
- package/dist/cli/commands/introspect.d.mts +1 -0
- package/dist/cli/commands/introspect.mjs +2 -1
- package/dist/cli/index.d.mts +3 -2
- package/dist/cli/index.mjs +3 -2
- package/dist/{constants-DNre0wRg.mjs → constants-J99LGTJI.mjs} +1 -0
- package/dist/context/index.d.mts +1 -0
- package/dist/context/index.mjs +3 -2
- package/dist/core/index.d.mts +3 -2
- package/dist/core/index.mjs +7 -6
- package/dist/{core-poalJLvp.mjs → core-BwwmNSp7.mjs} +3 -2
- package/dist/{createActionRouter-BxuywVRB.mjs → createActionRouter-DkJZWo0m.mjs} +14 -10
- package/dist/{createAggregationRouter-BiY-lGdY.mjs → createAggregationRouter-B1exOVD8.mjs} +10 -7
- package/dist/{createApp-CXrKFciH.mjs → createApp-Bv0m-Q6_.mjs} +625 -229
- package/dist/{defineAggregation-ufWjUwml.mjs → defineAggregation-gX4No1gt.mjs} +1 -0
- package/dist/{defineResource-CxWqDCAy.mjs → defineResource-XFPPUu2c.mjs} +84 -50
- package/dist/discovery/index.d.mts +1 -0
- package/dist/discovery/index.mjs +1 -0
- package/dist/docs/index.d.mts +3 -2
- package/dist/docs/index.mjs +3 -2
- package/dist/{elevation-C9s5hFP_.d.mts → elevation-BXCfVcNG.d.mts} +1 -0
- package/dist/{elevation-CIz1GjGm.mjs → elevation-DNXXxgjl.mjs} +4 -3
- package/dist/encryption/index.d.mts +2 -1
- package/dist/encryption/index.mjs +5 -3
- package/dist/{errorHandler-Bq2OeEQL.mjs → errorHandler-B2AZUJUj.mjs} +3 -2
- package/dist/{errorHandler-C6yv4ZTW.d.mts → errorHandler-CpYUJmPA.d.mts} +1 -0
- package/dist/{errors-DHcHVAgk.d.mts → errors-BGdVwP25.d.mts} +1 -0
- package/dist/{errors-DkO8Nk6-.mjs → errors-Bh1KK5ps.mjs} +1 -0
- package/dist/{eventPlugin-D2NNR4py.mjs → eventPlugin-Dk_TyXeE.mjs} +8 -6
- package/dist/{eventPlugin-Bg3cP2_u.d.mts → eventPlugin-GF5nnBIq.d.mts} +2 -1
- package/dist/events/index.d.mts +112 -44
- package/dist/events/index.mjs +177 -141
- package/dist/events/transports/redis-stream-entry.d.mts +2 -1
- package/dist/events/transports/redis-stream-entry.mjs +14 -9
- package/dist/events/transports/redis.d.mts +2 -1
- package/dist/events/transports/redis.mjs +1 -0
- package/dist/{externalPaths-BE6wFpYv.d.mts → externalPaths-CmGOxOtD.d.mts} +1 -0
- package/dist/factory/index.d.mts +9 -5
- package/dist/factory/index.mjs +131 -7
- package/dist/fastify-augmentation-BppG5bMT.d.mts +179 -0
- package/dist/{fields-CQ_7KQWw.mjs → fields-DEi65pGj.mjs} +6 -10
- package/dist/{fields-D3lhgzhD.d.mts → fields-I_EeRWND.d.mts} +1 -0
- package/dist/{generate-D0wZupBg.mjs → generate-CRSG0jTv.mjs} +2 -1
- package/dist/{generate-BnsL9GBm.d.mts → generate-DCGuEbqI.d.mts} +1 -0
- package/dist/{headers-BvlQSDMB.mjs → headers-BXjQYbeC.mjs} +2 -1
- package/dist/{health-CB_m4S32.mjs → health-5XnTa2uo.mjs} +1 -0
- package/dist/hooks/index.d.mts +2 -1
- package/dist/hooks/index.mjs +2 -1
- package/dist/idempotency/index.d.mts +3 -2
- package/dist/idempotency/index.mjs +8 -5
- package/dist/idempotency/redis.d.mts +2 -1
- package/dist/idempotency/redis.mjs +1 -0
- package/dist/{index-CG4yqMDB.d.mts → index-C-481Zf2.d.mts} +74 -18
- package/dist/{index-Q5gzEIWY.d.mts → index-CpzxqePp.d.mts} +107 -32
- package/dist/{index-Cx7bro4R.d.mts → index-DXlHwvuE.d.mts} +5 -4
- package/dist/index-ZWhLTp_s.d.mts +290 -0
- package/dist/{index-Ch16aAah.d.mts → index-zq6VrjaB.d.mts} +122 -15
- package/dist/index.d.mts +9 -8
- package/dist/index.mjs +11 -10
- package/dist/{init-tlRSt667.mjs → init-BbfP8aky.mjs} +25 -27
- package/dist/{init-BPEu8cs-.d.mts → init-Cscs4lab.d.mts} +1 -0
- package/dist/integrations/event-gateway.d.mts +3 -2
- package/dist/integrations/event-gateway.mjs +2 -1
- package/dist/integrations/index.d.mts +4 -3
- package/dist/integrations/index.mjs +1 -0
- package/dist/integrations/jobs/index.d.mts +2 -1
- package/dist/integrations/jobs/index.mjs +109 -33
- package/dist/integrations/mcp/index.d.mts +6 -6
- package/dist/integrations/mcp/index.mjs +2 -1
- package/dist/integrations/mcp/testing.d.mts +3 -2
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +1 -0
- package/dist/integrations/streamline.mjs +3 -2
- package/dist/integrations/webhooks.d.mts +1 -0
- package/dist/integrations/webhooks.mjs +1 -0
- package/dist/integrations/websocket-pushref-redis.d.mts +2 -1
- package/dist/integrations/websocket-pushref-redis.mjs +1 -0
- package/dist/integrations/websocket-redis.d.mts +2 -1
- package/dist/integrations/websocket-redis.mjs +1 -0
- package/dist/integrations/websocket.d.mts +3 -2
- package/dist/integrations/websocket.mjs +38 -34
- package/dist/{interface-BGZCzOGL.d.mts → interface-BOk3YnTz.d.mts} +1 -0
- package/dist/{interface-n5hpKg1d.mjs → interface-BP4IJCBO.mjs} +1 -0
- package/dist/{interface-Uk8ONJv7.d.mts → interface-DEWNOMza.d.mts} +1 -0
- package/dist/{jsonBody-BlzAoGCe.mjs → jsonBody-DDSR2HFl.mjs} +2 -1
- package/dist/{keys-Cd316u_y.mjs → keys-B-HBTGXl.mjs} +1 -0
- package/dist/{loadResources-DpU8pfjV.mjs → loadResources-DisEAmkm.mjs} +4 -2
- package/dist/{loadResourcesFromEntry-kvGT6EcO.mjs → loadResourcesFromEntry-D1MJp13f.mjs} +2 -1
- package/dist/logger/index.d.mts +1 -0
- package/dist/logger/index.mjs +1 -0
- package/dist/{mcpPlugin-nRzm5g2s.mjs → mcpPlugin-CpfVpKHE.mjs} +38 -38
- package/dist/{memory-CmnzXE5Z.mjs → memory-DsuWnj4T.mjs} +5 -4
- package/dist/{metrics-fdxI5mn_.mjs → metrics-DdQL8syd.mjs} +2 -1
- package/dist/middleware/index.d.mts +2 -1
- package/dist/middleware/index.mjs +3 -2
- package/dist/migrations/index.d.mts +1 -0
- package/dist/migrations/index.mjs +3 -2
- package/dist/module-Br81am6m.mjs +78 -0
- package/dist/{multipartBody-D0eBvkeq.mjs → multipartBody-BnkBLN7v.mjs} +3 -2
- package/dist/{openapi-B8Vkbxbn.mjs → openapi-DIRzvxC0.mjs} +6 -5
- package/dist/outbox-admin/index.d.mts +85 -0
- package/dist/outbox-admin/index.mjs +115 -0
- package/dist/permissions/index.d.mts +5 -4
- package/dist/permissions/index.mjs +6 -5
- package/dist/{permissions-Bfu_8y1U.mjs → permissions-Dl_m0NCe.mjs} +308 -42
- package/dist/{pipe-BOLmGIxa.mjs → pipe-ELHCP3zj.mjs} +3 -4
- package/dist/pipeline/index.d.mts +2 -1
- package/dist/pipeline/index.mjs +2 -1
- package/dist/{pipeline-BJnpkY9i.mjs → pipeline-CPA-jx0M.mjs} +17 -14
- package/dist/plugins/index.d.mts +8 -7
- package/dist/plugins/index.mjs +16 -15
- package/dist/plugins/response-cache.d.mts +1 -0
- package/dist/plugins/response-cache.mjs +2 -1
- package/dist/plugins/tracing-entry.d.mts +2 -1
- package/dist/plugins/tracing-entry.mjs +2 -1
- package/dist/{pluralize-BhrgwZb9.mjs → pluralize-C2ZkKbsn.mjs} +1 -0
- package/dist/{preloadResources-DSY5HGpS.mjs → preloadResources-CDs87fTw.mjs} +1 -0
- package/dist/{preloadResources-DCfa2egz.d.mts → preloadResources-T1hmlTEZ.d.mts} +117 -12
- package/dist/presets/filesUpload.d.mts +6 -5
- package/dist/presets/filesUpload.mjs +10 -12
- package/dist/presets/index.d.mts +3 -2
- package/dist/presets/index.mjs +2 -1
- package/dist/presets/multiTenant.d.mts +2 -1
- package/dist/presets/multiTenant.mjs +3 -2
- package/dist/presets/search.d.mts +3 -2
- package/dist/presets/search.mjs +2 -1
- package/dist/{presets-CJOEXLuk.mjs → presets-Dntd_pUO.mjs} +6 -4
- package/dist/{projection-UBDgfMwu.mjs → projection-CjHhM5W1.mjs} +2 -1
- package/dist/{pushref-registry-BbcnpNTm.d.mts → pushref-registry-Dov9l4qE.d.mts} +1 -0
- package/dist/{queryCachePlugin-br4otiqF.d.mts → queryCachePlugin-BzqxconG.d.mts} +3 -2
- package/dist/{queryCachePlugin-DSGuSVLW.mjs → queryCachePlugin-Dhps-Ldt.mjs} +6 -5
- package/dist/{rateLimit-DrED-C4v.mjs → rateLimit-C7mmAGs3.mjs} +7 -3
- package/dist/{rateLimitKey-BoWRH3e7.mjs → rateLimitKey-CccEFBfk.mjs} +1 -0
- package/dist/{realtime-B9nmIY5V.mjs → realtime-DVvVZZFz.mjs} +20 -9
- package/dist/{redis-UDtLaNj6.d.mts → redis-CduhKsJi.d.mts} +2 -1
- package/dist/{redis-stream-uIaauNpZ.d.mts → redis-stream-qN6A-RFv.d.mts} +2 -1
- package/dist/{registerRoute-JkxIEBJq.mjs → registerRoute-B_QrjYZS.mjs} +10 -7
- package/dist/registry/index.d.mts +3 -2
- package/dist/registry/index.mjs +3 -2
- package/dist/{registry-BYXUUg21.mjs → registry-DqUmzA9W.mjs} +2 -1
- package/dist/{renewingLease-3cuQH7h8.mjs → renewingLease-BpP9wwj7.mjs} +1 -0
- package/dist/{replyHelpers-67tRawQZ.mjs → replyHelpers-DS-P0b32.mjs} +2 -1
- package/dist/{repositoryFilter-Dz8k3S4P.mjs → repositoryFilter-DotlxG3L.mjs} +1 -0
- package/dist/{requestContext-MrlJwECf.mjs → requestContext-B6vQF-sy.mjs} +1 -0
- package/dist/{rolldown-runtime-8H4AJuhK.mjs → rolldown-runtime-BhflRrBs.mjs} +1 -0
- package/dist/{schedules-CjOd2MG9.mjs → schedules-C2Q3uv2d.mjs} +3 -2
- package/dist/{schemaConverter-DJvodiHj.mjs → schemaConverter-Crjvczi_.mjs} +70 -17
- package/dist/{schemaIR-Ca7ySNPx.mjs → schemaIR-CnyKK_1j.mjs} +2 -1
- package/dist/schemas/index.d.mts +1 -0
- package/dist/schemas/index.mjs +1 -0
- package/dist/scim/index.d.mts +1 -0
- package/dist/scim/index.mjs +2 -1
- package/dist/scope/index.d.mts +3 -2
- package/dist/scope/index.mjs +5 -4
- package/dist/{scope-DfaPntpo.mjs → scope-afz_Ieek.mjs} +4 -3
- package/dist/{sessionManager-BHOM3N_0.d.mts → sessionManager-qimlKV0P.d.mts} +1 -0
- package/dist/{simpleEqualityMatcher-Cg05HPsn.mjs → simpleEqualityMatcher-C4KiMWUa.mjs} +1 -0
- package/dist/{sse-BQ1w0dfl.mjs → sse-B9P4Y-UI.mjs} +7 -5
- package/dist/{sseStream-BQ7Qaynp.mjs → sseStream-B3zb_d1h.mjs} +2 -1
- package/dist/{storage-DLUZ04kK.d.mts → storage-Cx9Sz3Ou.d.mts} +1 -0
- package/dist/{store-helpers-DY6xq4sM.mjs → store-helpers-DmEMHlPQ.mjs} +1 -0
- package/dist/{streaming-BQcW6_Kq.mjs → streaming-DPZdQmDz.mjs} +1 -0
- package/dist/{subscribe-helpers-DfzsivM6.mjs → subscribe-helpers-sp5xTDVA.mjs} +2 -1
- package/dist/sync/changelog.d.mts +1 -0
- package/dist/sync/changelog.mjs +1 -0
- package/dist/testing/cleanupStoreContract.d.mts +2 -1
- package/dist/testing/cleanupStoreContract.mjs +10 -5
- package/dist/testing/index.d.mts +166 -12
- package/dist/testing/index.mjs +315 -43
- package/dist/testing/outboxStoreContract.d.mts +12 -2
- package/dist/testing/outboxStoreContract.mjs +110 -20
- package/dist/testing/storageContract.d.mts +2 -1
- package/dist/testing/storageContract.mjs +11 -5
- package/dist/{tracing-CsGKQJGg.d.mts → tracing-Dro5eH6j.d.mts} +1 -0
- package/dist/{transactionContext-PNdoyLLm.mjs → transactionContext-BvLKvOIL.mjs} +1 -0
- package/dist/{typeGuards-C9KXUly6.mjs → typeGuards-B_5-hKjJ.mjs} +1 -0
- package/dist/types/index.d.mts +6 -5
- package/dist/types/index.mjs +1 -0
- package/dist/types/storage.d.mts +2 -1
- package/dist/types/storage.mjs +1 -0
- package/dist/{types-ChvvVNP8.d.mts → types-B7XeLEgK.d.mts} +34 -2
- package/dist/{types-Cc91nXuA.d.mts → types-ByHn9rEO.d.mts} +1 -0
- package/dist/{types-C_ZZRcD6.d.mts → types-C4QE8qnm.d.mts} +1 -0
- package/dist/{types-D_n8L6VY.d.mts → types-CT5vu6M0.d.mts} +132 -17
- package/dist/{types-DjC-KUCz.d.mts → types-ChuShpCP.d.mts} +1 -0
- package/dist/{types-Bmmn-fO0.d.mts → types-CwcUQ610.d.mts} +2 -6
- package/dist/{types-CSyCxl2V.mjs → types-DXYX7dIR.mjs} +4 -3
- package/dist/usage/index.d.mts +3 -2
- package/dist/usage/index.mjs +0 -0
- package/dist/{userHelpers-Cg_y3fBz.mjs → userHelpers-BNsOXzOd.mjs} +1 -0
- package/dist/utils/index.d.mts +16 -7
- package/dist/utils/index.mjs +10 -9
- package/dist/{utils-taUVD4p3.mjs → utils-BtOha_Sp.mjs} +3 -2
- package/dist/{validate-Bk44F2yT.mjs → validate-PMCPR9El.mjs} +2 -1
- package/dist/{versioning-gKWyryT3.mjs → versioning-CKv2jz_U.mjs} +4 -3
- package/dist/{versioning-DXgY5BUm.d.mts → versioning-D_rhHfZm.d.mts} +5 -2
- package/dist/{websocket-_Pd0CgR0.d.mts → websocket-BOXVggMf.d.mts} +2 -1
- package/package.json +557 -539
- package/skills/arc/SKILL.md +22 -8
- package/skills/arc/references/api-reference.md +4 -0
- package/skills/arc/references/auth.md +3 -0
- package/skills/arc/references/integrations.md +33 -0
- package/skills/arc/references/mcp.md +6 -7
- package/skills/arc/references/production.md +24 -25
- package/skills/arc-code-review/references/anti-patterns.md +3 -3
- package/dist/fastify-augmentation-0WhPnUH7.d.mts +0 -76
- package/dist/index-CCzVR-N3.d.mts +0 -107
- package/dist/module-_-M7C5ES.mjs +0 -29
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-BhflRrBs.mjs";
|
|
2
3
|
import { arcLog } from "./logger/index.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { a as
|
|
6
|
-
import { t as MemoryCacheStore } from "./memory-
|
|
4
|
+
import { a as conjoinPolicyFilters, i as normalizeToDecision } from "./authorizationDecision-__T3EJDA.mjs";
|
|
5
|
+
import { n as getUserRoles } from "./userHelpers-BNsOXzOd.mjs";
|
|
6
|
+
import { C as isService, S as isOrgInScope, a as getDPoPJkt, b as isElevated, d as getScopeContext, f as getScopeContextMap, g as getUserId, m as getTeamId, n as PUBLIC_SCOPE, o as getMandate, p as getServiceScopes, u as getRequestScope, v as hasOrgAccess, x as isMember } from "./types-DXYX7dIR.mjs";
|
|
7
|
+
import { t as MemoryCacheStore } from "./memory-DsuWnj4T.mjs";
|
|
7
8
|
import { randomUUID } from "node:crypto";
|
|
8
9
|
//#region src/permissions/context.ts
|
|
9
10
|
/**
|
|
@@ -106,6 +107,7 @@ function requireAuth() {
|
|
|
106
107
|
if (!ctx.user) return deny("Authentication required");
|
|
107
108
|
return true;
|
|
108
109
|
};
|
|
110
|
+
check._requiresAuth = true;
|
|
109
111
|
return check;
|
|
110
112
|
}
|
|
111
113
|
function requireRoles(rolesOrFirst, optionsOrSecond, ...rest) {
|
|
@@ -139,6 +141,36 @@ function requireRoles(rolesOrFirst, optionsOrSecond, ...rest) {
|
|
|
139
141
|
return check;
|
|
140
142
|
}
|
|
141
143
|
/**
|
|
144
|
+
* Require a PLATFORM role — global roles only. An organization role can never
|
|
145
|
+
* satisfy this gate.
|
|
146
|
+
*
|
|
147
|
+
* `requireRoles(["ops"])` also accepts an ORG role named `ops` (that is its
|
|
148
|
+
* documented default), and `requireOrgRole("manager")` grants ANY organization's
|
|
149
|
+
* manager. Both return a bare allow with no policy, so a surface that is global
|
|
150
|
+
* by construction — a queue dashboard, a cluster-wide operations endpoint —
|
|
151
|
+
* cannot distinguish them from a real operator gate by looking at the decision.
|
|
152
|
+
* That makes "platform-only" a property the check has to DECLARE, which is what
|
|
153
|
+
* `_platformOnly` records and what such a surface can then require.
|
|
154
|
+
*
|
|
155
|
+
* Elevated scope passes: platform admin is the identity this gate is for.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* permissions: { list: requirePlatformRole("platform-ops") }
|
|
159
|
+
*/
|
|
160
|
+
function requirePlatformRole(...args) {
|
|
161
|
+
const roles = normalizeVariadicOrArray(args);
|
|
162
|
+
const check = (ctx) => {
|
|
163
|
+
if (isElevated(scopeOf(ctx))) return true;
|
|
164
|
+
if (!ctx.user) return deny("Authentication required");
|
|
165
|
+
const userRoles = getUserRoles(ctx.user);
|
|
166
|
+
if (roles.some((r) => userRoles.includes(r))) return true;
|
|
167
|
+
return deny(`Required platform roles: ${roles.join(", ")}`);
|
|
168
|
+
};
|
|
169
|
+
check._roles = roles;
|
|
170
|
+
check._platformOnly = true;
|
|
171
|
+
return check;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
142
174
|
* Short-form alias of `requireRoles()`. Identical behavior — checks both
|
|
143
175
|
* platform roles AND org roles. Prefer `requireRoles` for new code; this
|
|
144
176
|
* exists for call sites that want a terser name.
|
|
@@ -234,6 +266,15 @@ function allOf(...checks) {
|
|
|
234
266
|
if (rb?._roles?.length) check._roles = [...rb._roles];
|
|
235
267
|
if (rb?._orgRoles?.length) check._orgRoles = [...rb._orgRoles];
|
|
236
268
|
}
|
|
269
|
+
const scopeContext = {};
|
|
270
|
+
let scopeConflict = false;
|
|
271
|
+
for (const c of checks) for (const [dim, val] of Object.entries(c._scopeContext ?? {})) if (dim in scopeContext && scopeContext[dim] !== val) scopeConflict = true;
|
|
272
|
+
else scopeContext[dim] = val;
|
|
273
|
+
if (Object.keys(scopeContext).length > 0) check._scopeContext = scopeContext;
|
|
274
|
+
const hasMetaSurface = (c) => Boolean(c._isPublic || c._roles?.length || c._orgRoles?.length || c._scopeContext && Object.keys(c._scopeContext).length > 0);
|
|
275
|
+
const opaqueBranch = checks.some((c) => !hasMetaSurface(c) && !c._requiresAuth);
|
|
276
|
+
const childConditional = checks.some((c) => c._conditional);
|
|
277
|
+
if (opaqueBranch || scopeConflict || childConditional) check._conditional = true;
|
|
237
278
|
return check;
|
|
238
279
|
}
|
|
239
280
|
/**
|
|
@@ -269,6 +310,7 @@ function anyOf(...checks) {
|
|
|
269
310
|
}
|
|
270
311
|
if (roles.size > 0) check._roles = [...roles];
|
|
271
312
|
if (orgRoles.size > 0) check._orgRoles = [...orgRoles];
|
|
313
|
+
if (checks.some((c) => c._conditional)) check._conditional = true;
|
|
272
314
|
}
|
|
273
315
|
return check;
|
|
274
316
|
}
|
|
@@ -536,7 +578,8 @@ function requireAgentScope(opts) {
|
|
|
536
578
|
*/
|
|
537
579
|
function requireOrgMembership() {
|
|
538
580
|
const check = (ctx) => {
|
|
539
|
-
|
|
581
|
+
const scope = scopeOf(ctx);
|
|
582
|
+
if (hasOrgAccess(scope)) return true;
|
|
540
583
|
if (!ctx.user) return deny("Authentication required");
|
|
541
584
|
return deny("Organization membership required");
|
|
542
585
|
};
|
|
@@ -818,20 +861,66 @@ function createDynamicPermissionMatrix(config) {
|
|
|
818
861
|
}) : void 0;
|
|
819
862
|
const cacheStore = config.cacheStore ?? internalStore;
|
|
820
863
|
const trackedKeys = /* @__PURE__ */ new Set();
|
|
864
|
+
const orgKeyIndex = /* @__PURE__ */ new Map();
|
|
865
|
+
const maxTrackedKeys = config.cache?.maxEntries ?? 1e4;
|
|
866
|
+
/**
|
|
867
|
+
* Record a key and ENFORCE the cap in the same step.
|
|
868
|
+
*
|
|
869
|
+
* The cap has to live here rather than at the write site: a node that mostly
|
|
870
|
+
* READS a shared cache registers on every hit but may rarely set, so a
|
|
871
|
+
* cap applied only after a `set` never runs and the bookkeeping grows with
|
|
872
|
+
* every distinct org/user/role combination the node has ever seen — unbounded,
|
|
873
|
+
* and outliving the cache entries themselves, which the external store expires
|
|
874
|
+
* on its own schedule. Eviction is insertion-ordered (`Set` iteration order),
|
|
875
|
+
* so the oldest bookkeeping goes first; dropping a key only costs a missed
|
|
876
|
+
* local invalidation for an entry that will still expire by TTL.
|
|
877
|
+
*/
|
|
878
|
+
const registerKey = (orgId, key) => {
|
|
879
|
+
trackedKeys.add(key);
|
|
880
|
+
const bucket = orgId ?? "no-org";
|
|
881
|
+
let keys = orgKeyIndex.get(bucket);
|
|
882
|
+
if (!keys) {
|
|
883
|
+
keys = /* @__PURE__ */ new Set();
|
|
884
|
+
orgKeyIndex.set(bucket, keys);
|
|
885
|
+
}
|
|
886
|
+
keys.add(key);
|
|
887
|
+
if (trackedKeys.size > maxTrackedKeys) {
|
|
888
|
+
const overflow = trackedKeys.size - maxTrackedKeys;
|
|
889
|
+
const iter = trackedKeys.values();
|
|
890
|
+
for (let i = 0; i < overflow; i++) {
|
|
891
|
+
const oldest = iter.next().value;
|
|
892
|
+
if (oldest) forgetKey(oldest);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
const forgetKey = (key) => {
|
|
897
|
+
trackedKeys.delete(key);
|
|
898
|
+
for (const [bucket, keys] of orgKeyIndex) if (keys.delete(key) && keys.size === 0) orgKeyIndex.delete(bucket);
|
|
899
|
+
};
|
|
821
900
|
const nodeId = randomUUID().slice(0, 8);
|
|
822
901
|
const DEFAULT_EVENT_TYPE = "arc.permissions.invalidated";
|
|
823
902
|
let eventBridge = null;
|
|
824
903
|
async function localInvalidateByOrg(orgId) {
|
|
825
904
|
if (!cacheStore) return;
|
|
826
905
|
const prefix = `${orgId}::`;
|
|
827
|
-
|
|
828
|
-
|
|
906
|
+
if (cacheStore.clear) try {
|
|
907
|
+
await cacheStore.clear(`${prefix}*`);
|
|
908
|
+
const keys = orgKeyIndex.get(orgId);
|
|
909
|
+
if (keys) for (const k of keys) trackedKeys.delete(k);
|
|
910
|
+
orgKeyIndex.delete(orgId);
|
|
911
|
+
return;
|
|
912
|
+
} catch (error) {
|
|
913
|
+
logger.warn(`[DynamicPermissionMatrix] invalidateByOrg clear('${prefix}*') failed — falling back to per-key delete: ${error instanceof Error ? error.message : String(error)}`);
|
|
914
|
+
}
|
|
915
|
+
const toDelete = new Set(orgKeyIndex.get(orgId) ?? []);
|
|
916
|
+
for (const key of trackedKeys) if (key.startsWith(prefix)) toDelete.add(key);
|
|
829
917
|
for (const key of toDelete) try {
|
|
830
918
|
await cacheStore.delete(key);
|
|
831
|
-
|
|
919
|
+
forgetKey(key);
|
|
832
920
|
} catch (error) {
|
|
833
921
|
logger.warn(`[DynamicPermissionMatrix] invalidateByOrg delete failed for '${key}': ${error instanceof Error ? error.message : String(error)}`);
|
|
834
922
|
}
|
|
923
|
+
orgKeyIndex.delete(orgId);
|
|
835
924
|
}
|
|
836
925
|
function isActionAllowed(actions, action) {
|
|
837
926
|
if (!actions || actions.length === 0) return false;
|
|
@@ -851,27 +940,22 @@ function createDynamicPermissionMatrix(config) {
|
|
|
851
940
|
}
|
|
852
941
|
async function resolveMatrix(ctx, orgId, orgRoles) {
|
|
853
942
|
if (!cacheStore) return config.resolveRolePermissions(ctx);
|
|
854
|
-
const
|
|
943
|
+
const customKey = config.cache?.key?.(ctx);
|
|
944
|
+
const cacheKey = customKey ? `${orgId ?? "no-org"}::${(orgRoles ?? []).slice().sort().join(",")}::${customKey}` : buildDefaultCacheKey(ctx, orgId, orgRoles);
|
|
855
945
|
if (!cacheKey) return config.resolveRolePermissions(ctx);
|
|
856
946
|
try {
|
|
857
947
|
const hit = await cacheStore.get(cacheKey);
|
|
858
|
-
if (hit)
|
|
948
|
+
if (hit) {
|
|
949
|
+
registerKey(orgId, cacheKey);
|
|
950
|
+
return hit;
|
|
951
|
+
}
|
|
859
952
|
} catch (error) {
|
|
860
953
|
logger.warn(`[DynamicPermissionMatrix] Cache get failed for '${cacheKey}': ${error instanceof Error ? error.message : String(error)}`);
|
|
861
954
|
}
|
|
862
955
|
const value = await config.resolveRolePermissions(ctx);
|
|
863
956
|
try {
|
|
864
957
|
await cacheStore.set(cacheKey, value, cacheTtlSeconds);
|
|
865
|
-
|
|
866
|
-
const maxTracked = config.cache?.maxEntries ?? 1e4;
|
|
867
|
-
if (trackedKeys.size > maxTracked) {
|
|
868
|
-
const overflow = trackedKeys.size - maxTracked;
|
|
869
|
-
const iter = trackedKeys.values();
|
|
870
|
-
for (let i = 0; i < overflow; i++) {
|
|
871
|
-
const oldest = iter.next().value;
|
|
872
|
-
if (oldest) trackedKeys.delete(oldest);
|
|
873
|
-
}
|
|
874
|
-
}
|
|
958
|
+
registerKey(orgId, cacheKey);
|
|
875
959
|
} catch (error) {
|
|
876
960
|
logger.warn(`[DynamicPermissionMatrix] Cache set failed for '${cacheKey}': ${error instanceof Error ? error.message : String(error)}`);
|
|
877
961
|
}
|
|
@@ -889,7 +973,8 @@ function createDynamicPermissionMatrix(config) {
|
|
|
889
973
|
try {
|
|
890
974
|
matrix = await resolveMatrix(ctx, scope.organizationId, orgRoles);
|
|
891
975
|
} catch (error) {
|
|
892
|
-
|
|
976
|
+
logger.warn(`[DynamicPermissionMatrix] matrix resolution failed for org '${scope.organizationId ?? "?"}': ${error instanceof Error ? error.message : String(error)}`);
|
|
977
|
+
return deny("Permission policy is temporarily unavailable");
|
|
893
978
|
}
|
|
894
979
|
for (const [resource, actions] of Object.entries(required)) for (const action of actions) if (!orgRoles.some((role) => roleAllows(matrix, role, resource, action))) return deny(`Missing permission: ${resource}:${action}`);
|
|
895
980
|
return true;
|
|
@@ -925,6 +1010,7 @@ function createDynamicPermissionMatrix(config) {
|
|
|
925
1010
|
if (cacheStore.clear) try {
|
|
926
1011
|
await cacheStore.clear();
|
|
927
1012
|
trackedKeys.clear();
|
|
1013
|
+
orgKeyIndex.clear();
|
|
928
1014
|
return;
|
|
929
1015
|
} catch (error) {
|
|
930
1016
|
logger.warn(`[DynamicPermissionMatrix] cacheStore.clear failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -935,6 +1021,7 @@ function createDynamicPermissionMatrix(config) {
|
|
|
935
1021
|
logger.warn(`[DynamicPermissionMatrix] Cache delete failed for '${key}': ${error instanceof Error ? error.message : String(error)}`);
|
|
936
1022
|
}
|
|
937
1023
|
trackedKeys.clear();
|
|
1024
|
+
orgKeyIndex.clear();
|
|
938
1025
|
},
|
|
939
1026
|
async connectEvents(events, options) {
|
|
940
1027
|
if (eventBridge) await this.disconnectEvents();
|
|
@@ -975,15 +1062,30 @@ function createDynamicPermissionMatrix(config) {
|
|
|
975
1062
|
//#region src/permissions/explain.ts
|
|
976
1063
|
/**
|
|
977
1064
|
* Extract a check's static requirement from its `PermissionCheckMeta`. Public
|
|
978
|
-
* wins; then
|
|
979
|
-
*
|
|
1065
|
+
* wins; then a scope-context gate (optionally with roles); then the union of
|
|
1066
|
+
* platform + org roles a role gate declares; otherwise "authenticated"
|
|
1067
|
+
* (auth-required / custom / request-time checks). An `allOf` that couldn't be
|
|
1068
|
+
* fully captured statically carries `conditional: true` (see `_conditional`).
|
|
980
1069
|
*/
|
|
981
1070
|
function describePermission(check) {
|
|
982
1071
|
if (check._isPublic) return { kind: "public" };
|
|
983
1072
|
const roles = [...check._roles ?? [], ...check._orgRoles ?? []];
|
|
1073
|
+
const scope = check._scopeContext;
|
|
1074
|
+
const conditional = check._conditional ? true : void 0;
|
|
1075
|
+
if (scope && Object.keys(scope).length > 0) return roles.length > 0 ? {
|
|
1076
|
+
kind: "scoped",
|
|
1077
|
+
scope,
|
|
1078
|
+
roles,
|
|
1079
|
+
conditional
|
|
1080
|
+
} : {
|
|
1081
|
+
kind: "scoped",
|
|
1082
|
+
scope,
|
|
1083
|
+
conditional
|
|
1084
|
+
};
|
|
984
1085
|
if (roles.length > 0) return {
|
|
985
1086
|
kind: "roles",
|
|
986
|
-
roles
|
|
1087
|
+
roles,
|
|
1088
|
+
conditional
|
|
987
1089
|
};
|
|
988
1090
|
return { kind: "authenticated" };
|
|
989
1091
|
}
|
|
@@ -1008,23 +1110,63 @@ function explainAccess(check, principal) {
|
|
|
1008
1110
|
reason: "requires authentication; decided per request",
|
|
1009
1111
|
requirement
|
|
1010
1112
|
};
|
|
1113
|
+
const held = /* @__PURE__ */ new Set([...principal?.roles ?? [], ...principal?.orgRoles ?? []]);
|
|
1114
|
+
if (requirement.kind === "scoped") {
|
|
1115
|
+
const scopeReason = describeScope(requirement.scope);
|
|
1116
|
+
const rolesReason = requirement.roles?.length ? `role(s) ${requirement.roles.join(", ")} + ` : "";
|
|
1117
|
+
if (requirement.roles?.length && principal && !requirement.roles.some((r) => held.has(r))) return {
|
|
1118
|
+
decision: "deny",
|
|
1119
|
+
reason: `requires ${rolesReason}${scopeReason}`,
|
|
1120
|
+
requirement
|
|
1121
|
+
};
|
|
1122
|
+
if (!principal?.scope) return {
|
|
1123
|
+
decision: "conditional",
|
|
1124
|
+
reason: `requires ${rolesReason}${scopeReason}`,
|
|
1125
|
+
requirement
|
|
1126
|
+
};
|
|
1127
|
+
if (!Object.entries(requirement.scope).every(([dim, val]) => val === void 0 ? principal.scope?.[dim] !== void 0 : principal.scope?.[dim] === val)) return {
|
|
1128
|
+
decision: "deny",
|
|
1129
|
+
reason: `requires ${rolesReason}${scopeReason}`,
|
|
1130
|
+
requirement
|
|
1131
|
+
};
|
|
1132
|
+
if (requirement.conditional) return {
|
|
1133
|
+
decision: "conditional",
|
|
1134
|
+
reason: `in scope ${scopeReason}; additional runtime conditions apply`,
|
|
1135
|
+
requirement
|
|
1136
|
+
};
|
|
1137
|
+
return {
|
|
1138
|
+
decision: "allow",
|
|
1139
|
+
reason: `granted in scope: ${scopeReason}`,
|
|
1140
|
+
requirement
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
const rolesText = `requires one of: ${requirement.roles.join(", ")}`;
|
|
1011
1144
|
if (!principal) return {
|
|
1012
1145
|
decision: "conditional",
|
|
1013
|
-
reason:
|
|
1146
|
+
reason: rolesText,
|
|
1014
1147
|
requirement
|
|
1015
1148
|
};
|
|
1016
|
-
const held = /* @__PURE__ */ new Set([...principal.roles ?? [], ...principal.orgRoles ?? []]);
|
|
1017
1149
|
const matched = requirement.roles.filter((r) => held.has(r));
|
|
1018
|
-
|
|
1150
|
+
if (matched.length === 0) return {
|
|
1151
|
+
decision: "deny",
|
|
1152
|
+
reason: rolesText,
|
|
1153
|
+
requirement
|
|
1154
|
+
};
|
|
1155
|
+
if (requirement.conditional) return {
|
|
1156
|
+
decision: "conditional",
|
|
1157
|
+
reason: `has role(s) ${matched.join(", ")}; additional runtime conditions apply`,
|
|
1158
|
+
requirement
|
|
1159
|
+
};
|
|
1160
|
+
return {
|
|
1019
1161
|
decision: "allow",
|
|
1020
1162
|
reason: `granted via role(s): ${matched.join(", ")}`,
|
|
1021
1163
|
requirement
|
|
1022
|
-
} : {
|
|
1023
|
-
decision: "deny",
|
|
1024
|
-
reason: `requires one of: ${requirement.roles.join(", ")}`,
|
|
1025
|
-
requirement
|
|
1026
1164
|
};
|
|
1027
1165
|
}
|
|
1166
|
+
/** Render a scope-context requirement as a readable clause: `branchRole=head_office`. */
|
|
1167
|
+
function describeScope(scope) {
|
|
1168
|
+
return Object.entries(scope).map(([dim, val]) => val === void 0 ? `${dim} (any)` : `${dim}=${val}`).join(", ");
|
|
1169
|
+
}
|
|
1028
1170
|
/**
|
|
1029
1171
|
* Introspect a whole permission map (`{ list, get, create, ... }` or any
|
|
1030
1172
|
* action→check record) into `action → requirement`. The reusable core of a
|
|
@@ -1167,27 +1309,59 @@ function requireGrant(options) {
|
|
|
1167
1309
|
*/
|
|
1168
1310
|
function createRoleHierarchy(map) {
|
|
1169
1311
|
const cache = /* @__PURE__ */ new Map();
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1312
|
+
/**
|
|
1313
|
+
* Resolve a role's transitive expansion.
|
|
1314
|
+
*
|
|
1315
|
+
* `stack` is the CURRENT recursion path (a DFS stack), NOT a shared visited
|
|
1316
|
+
* accumulator: a role is added on entry and REMOVED on exit, so sibling
|
|
1317
|
+
* branches never pollute each other. Without this, a diamond `A→B→D`, `A→C→D`
|
|
1318
|
+
* would let B's traversal leave `D` in a shared set, making C's `D` look like a
|
|
1319
|
+
* back-edge and caching `C` WITHOUT `D` — a nondeterministic under-grant.
|
|
1320
|
+
*
|
|
1321
|
+
* Returns `cyclic` = whether a back-edge into `stack` was cut anywhere in the
|
|
1322
|
+
* subtree. A cyclic subtree's result depends on the entry path, so it is NOT
|
|
1323
|
+
* cached (recomputed each call — correct, if unmemoized). Acyclic subtrees (the
|
|
1324
|
+
* norm) cache safely.
|
|
1325
|
+
*/
|
|
1326
|
+
function resolveRole(role, stack) {
|
|
1327
|
+
if (stack.has(role)) return {
|
|
1328
|
+
roles: [],
|
|
1329
|
+
cyclic: true
|
|
1330
|
+
};
|
|
1173
1331
|
const cached = cache.get(role);
|
|
1174
|
-
if (cached) return
|
|
1332
|
+
if (cached) return {
|
|
1333
|
+
roles: cached,
|
|
1334
|
+
cyclic: false
|
|
1335
|
+
};
|
|
1175
1336
|
const children = map[role];
|
|
1176
1337
|
if (!children || children.length === 0) {
|
|
1177
1338
|
cache.set(role, [role]);
|
|
1178
|
-
return
|
|
1339
|
+
return {
|
|
1340
|
+
roles: [role],
|
|
1341
|
+
cyclic: false
|
|
1342
|
+
};
|
|
1179
1343
|
}
|
|
1344
|
+
stack.add(role);
|
|
1180
1345
|
const result = [role];
|
|
1181
|
-
|
|
1346
|
+
let cyclic = false;
|
|
1347
|
+
for (const child of children) {
|
|
1348
|
+
const childResult = resolveRole(child, stack);
|
|
1349
|
+
result.push(...childResult.roles);
|
|
1350
|
+
if (childResult.cyclic) cyclic = true;
|
|
1351
|
+
}
|
|
1352
|
+
stack.delete(role);
|
|
1182
1353
|
const deduped = [...new Set(result)];
|
|
1183
|
-
cache.set(role, deduped);
|
|
1184
|
-
return
|
|
1354
|
+
if (!cyclic) cache.set(role, deduped);
|
|
1355
|
+
return {
|
|
1356
|
+
roles: deduped,
|
|
1357
|
+
cyclic
|
|
1358
|
+
};
|
|
1185
1359
|
}
|
|
1186
1360
|
return {
|
|
1187
1361
|
expand(roles) {
|
|
1188
1362
|
if (roles.length === 0) return [];
|
|
1189
1363
|
const all = /* @__PURE__ */ new Set();
|
|
1190
|
-
for (const role of roles) for (const expanded of resolveRole(role, /* @__PURE__ */ new Set())) all.add(expanded);
|
|
1364
|
+
for (const role of roles) for (const expanded of resolveRole(role, /* @__PURE__ */ new Set()).roles) all.add(expanded);
|
|
1191
1365
|
return [...all];
|
|
1192
1366
|
},
|
|
1193
1367
|
includes(userRoles, requiredRole) {
|
|
@@ -1372,4 +1546,96 @@ function readOnly(overrides) {
|
|
|
1372
1546
|
}, overrides);
|
|
1373
1547
|
}
|
|
1374
1548
|
//#endregion
|
|
1375
|
-
|
|
1549
|
+
//#region src/permissions/matrix.ts
|
|
1550
|
+
/**
|
|
1551
|
+
* Permission MATRIX — what the caller may do, answered from the live registry.
|
|
1552
|
+
*
|
|
1553
|
+
* A permission-gated frontend has to decide which screens and buttons to render
|
|
1554
|
+
* before it issues a request. Without a server answer it keeps a hand-written
|
|
1555
|
+
* copy of the gates, and that copy drifts the moment a resource changes one —
|
|
1556
|
+
* silently, and in the dangerous direction: the UI offers an action the server
|
|
1557
|
+
* will refuse, or hides one the user is entitled to.
|
|
1558
|
+
*
|
|
1559
|
+
* This builds the answer from what arc ACTUALLY enforces. Every registered
|
|
1560
|
+
* resource contributes its CRUD gates, its state-transition `action:<verb>`
|
|
1561
|
+
* gates and its `agg:<name>` gates, introspected through `describePermission`.
|
|
1562
|
+
* A resource that exists therefore gates its own screen with no one adding it
|
|
1563
|
+
* anywhere — including resources unique to one deployment.
|
|
1564
|
+
*
|
|
1565
|
+
* ## Why it lives in arc
|
|
1566
|
+
*
|
|
1567
|
+
* Every input is arc's: `describePermission`, the resource registry, the shapes
|
|
1568
|
+
* of `actions` and `aggregations`. Nothing here knows what a domain is. Built
|
|
1569
|
+
* in a host, it is re-derived per deployment — and the known gap below is a
|
|
1570
|
+
* gap in arc's own introspection, which a host cannot close but arc can.
|
|
1571
|
+
*
|
|
1572
|
+
* ## Known gap
|
|
1573
|
+
*
|
|
1574
|
+
* `anyOf(a, b)` composes checks into a bare function with no aggregated meta,
|
|
1575
|
+
* so `anyOf(platformAdminOnly(), requireOrgRole(…))` introspects as
|
|
1576
|
+
* `authenticated` with no roles. Consumers must read `authenticated` as "the
|
|
1577
|
+
* server decides" rather than "any logged-in user", and fall back to an
|
|
1578
|
+
* explicit gate for those. Fixing it means teaching the combinators to
|
|
1579
|
+
* aggregate meta — an arc change, tracked here because this is where it shows.
|
|
1580
|
+
*/
|
|
1581
|
+
/** Re-shape arc's typed requirement into the wire entry. */
|
|
1582
|
+
function introspectCheck(check) {
|
|
1583
|
+
const req = describePermission(check);
|
|
1584
|
+
if (req.kind === "public") return {
|
|
1585
|
+
type: "public",
|
|
1586
|
+
roles: []
|
|
1587
|
+
};
|
|
1588
|
+
if (req.kind === "roles") return {
|
|
1589
|
+
type: "roles",
|
|
1590
|
+
roles: [...req.roles]
|
|
1591
|
+
};
|
|
1592
|
+
if (req.kind === "scoped") return {
|
|
1593
|
+
type: "scoped",
|
|
1594
|
+
roles: req.roles ? [...req.roles] : [],
|
|
1595
|
+
scope: { ...req.scope }
|
|
1596
|
+
};
|
|
1597
|
+
return {
|
|
1598
|
+
type: "authenticated",
|
|
1599
|
+
roles: []
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
/** Introspect a permission module, flattening nested groups to `a.b`. */
|
|
1603
|
+
function introspectModule(mod) {
|
|
1604
|
+
const result = {};
|
|
1605
|
+
for (const [action, check] of Object.entries(mod)) if (typeof check === "function") result[action] = introspectCheck(check);
|
|
1606
|
+
else if (typeof check === "object" && check !== null) for (const [nested, entry] of Object.entries(introspectModule(check))) result[`${action}.${nested}`] = entry;
|
|
1607
|
+
return result;
|
|
1608
|
+
}
|
|
1609
|
+
/** Every live resource's ACTUAL gates, keyed by arc resource name. */
|
|
1610
|
+
function introspectRegistry(registry) {
|
|
1611
|
+
const modules = {};
|
|
1612
|
+
for (const entry of registry.getAll()) {
|
|
1613
|
+
const map = {};
|
|
1614
|
+
for (const [action, check] of Object.entries(entry.permissions ?? {})) if (typeof check === "function") map[action] = introspectCheck(check);
|
|
1615
|
+
for (const a of entry.actions ?? []) if (typeof a.permissions === "function") map[`action:${a.name}`] = introspectCheck(a.permissions);
|
|
1616
|
+
for (const agg of entry.aggregations ?? []) if (typeof agg.permissions === "function") map[`agg:${agg.name}`] = introspectCheck(agg.permissions);
|
|
1617
|
+
if (Object.keys(map).length > 0) modules[entry.name] = map;
|
|
1618
|
+
}
|
|
1619
|
+
return modules;
|
|
1620
|
+
}
|
|
1621
|
+
function buildPermissionMatrix(options = {}) {
|
|
1622
|
+
const platform = [...options.platformRoles ?? []];
|
|
1623
|
+
const branch = [...options.orgRoles ?? []];
|
|
1624
|
+
const modules = {};
|
|
1625
|
+
for (const [name, mod] of Object.entries(options.curatedModules ?? {})) if (typeof mod === "object" && mod !== null) modules[name] = introspectModule(mod);
|
|
1626
|
+
for (const [name, mod] of Object.entries(options.fallbackModules ?? {})) {
|
|
1627
|
+
if (modules[name]) continue;
|
|
1628
|
+
if (typeof mod === "object" && mod !== null) modules[name] = introspectModule(mod);
|
|
1629
|
+
}
|
|
1630
|
+
if (options.registry) Object.assign(modules, introspectRegistry(options.registry));
|
|
1631
|
+
return {
|
|
1632
|
+
roles: [.../* @__PURE__ */ new Set([...platform, ...branch])],
|
|
1633
|
+
roleGroups: {
|
|
1634
|
+
platform,
|
|
1635
|
+
branch
|
|
1636
|
+
},
|
|
1637
|
+
modules
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
//#endregion
|
|
1641
|
+
export { requireAgentScope as A, requireAuth as B, createOrgPermissions as C, requireScopeContext as D, requireOrgRole as E, allowPublic as F, when as G, requirePlatformRole as H, anyOf as I, scopeOf as K, deny as L, requireMandate as M, allOf as N, requireServiceScope as O, allow as P, denyAll as R, createDynamicPermissionMatrix as S, requireOrgMembership as T, requireRoles as U, requireOwnership as V, roles as W, requireGrant as _, adminOnly as a, describePermissionMap as b, ownerWithAdminBypass as c, publicRead as d, publicReadAdminWrite as f, modeSatisfies as g, GRANT_MODES as h, introspectRegistry as i, requireDPoP as j, requireTeamMembership as k, permissionMatrix as l, createRoleHierarchy as m, introspectCheck as n, authenticated as o, readOnly as p, introspectModule as r, fullPublic as s, buildPermissionMatrix as t, presets_exports as u, collectPublicSurface as v, requireOrgInScope as w, explainAccess as x, describePermission as y, not as z };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
|
|
2
|
+
import { r as ForbiddenError } from "./errors-Bh1KK5ps.mjs";
|
|
2
3
|
//#region src/pipeline/pipe.ts
|
|
3
4
|
/**
|
|
4
5
|
* Compose pipeline steps into an ordered array.
|
|
@@ -39,9 +40,7 @@ async function executePipeline(steps, ctx, handler, operation) {
|
|
|
39
40
|
case "transform":
|
|
40
41
|
transforms.push(step);
|
|
41
42
|
break;
|
|
42
|
-
case "interceptor":
|
|
43
|
-
interceptors.push(step);
|
|
44
|
-
break;
|
|
43
|
+
case "interceptor": interceptors.push(step);
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
46
|
for (const g of guards) if (!await g.handler(ctx)) throw new ForbiddenError(`Guard '${g.name}' denied access`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
import { Ct as PipelineStep, Rt as IControllerResponse, St as PipelineContext, _t as Guard, bt as OperationFilter, vt as Interceptor, wt as Transform, xt as PipelineConfig, yt as NextFunction } from "../index-CpzxqePp.mjs";
|
|
2
3
|
//#region src/pipeline/guard.d.ts
|
|
3
4
|
interface GuardOptions {
|
|
4
5
|
operations?: OperationFilter;
|
package/dist/pipeline/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
|
|
2
|
+
import { n as pipe, t as executePipeline } from "../pipe-ELHCP3zj.mjs";
|
|
2
3
|
//#region src/pipeline/guard.ts
|
|
3
4
|
/**
|
|
4
5
|
* Create a named guard.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { t as
|
|
5
|
-
import { t as
|
|
1
|
+
/*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
|
|
2
|
+
import { n as getUserRoles } from "./userHelpers-BNsOXzOd.mjs";
|
|
3
|
+
import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-DXYX7dIR.mjs";
|
|
4
|
+
import { i as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-DEi65pGj.mjs";
|
|
5
|
+
import { t as executePipeline } from "./pipe-ELHCP3zj.mjs";
|
|
6
|
+
import { t as buildRequestScopeProjection } from "./projection-CjHhM5W1.mjs";
|
|
6
7
|
import { isPaginatedResult, toCanonicalList } from "@classytic/repo-core/pagination";
|
|
7
8
|
//#region src/core/fastifyAdapter.ts
|
|
8
9
|
/** Type guard for Mongoose-like documents with toObject() */
|
|
@@ -121,9 +122,7 @@ function computeFieldCapabilities(fieldPerms, effectiveRoles) {
|
|
|
121
122
|
case "visibleTo":
|
|
122
123
|
readable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
|
|
123
124
|
break;
|
|
124
|
-
case "writableBy":
|
|
125
|
-
writable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
|
|
126
|
-
break;
|
|
125
|
+
case "writableBy": writable = perm.roles?.some((r) => effectiveRoles.includes(r)) ?? false;
|
|
127
126
|
}
|
|
128
127
|
caps[field] = {
|
|
129
128
|
readable,
|
|
@@ -171,8 +170,10 @@ function sendControllerResponse(reply, response, request) {
|
|
|
171
170
|
/** Apply both field mask and field-level permissions to a data item */
|
|
172
171
|
const applyPermissions = (data) => {
|
|
173
172
|
let result = fieldMaskConfig ? applyFieldMask(data, fieldMaskConfig) : data;
|
|
174
|
-
if (fieldPerms && result && typeof result === "object")
|
|
175
|
-
|
|
173
|
+
if (fieldPerms && result && typeof result === "object") {
|
|
174
|
+
if (Array.isArray(result)) result = result.map((item) => applyFieldReadPermissions(item, fieldPerms, effectiveRoles));
|
|
175
|
+
else result = applyFieldReadPermissions(result, fieldPerms, effectiveRoles);
|
|
176
|
+
}
|
|
176
177
|
return result;
|
|
177
178
|
};
|
|
178
179
|
if (response.headers) for (const [key, value] of Object.entries(response.headers)) reply.header(key, value);
|
|
@@ -280,11 +281,12 @@ function resolvePipelineSteps(pipeline, operation) {
|
|
|
280
281
|
*/
|
|
281
282
|
function buildPipelineHandler(controllerMethod, steps, operation, resourceName) {
|
|
282
283
|
return async (req, reply) => {
|
|
283
|
-
|
|
284
|
+
const pipeCtx = {
|
|
284
285
|
...createRequestContext(req),
|
|
285
286
|
resource: resourceName,
|
|
286
287
|
operation
|
|
287
|
-
}
|
|
288
|
+
};
|
|
289
|
+
sendControllerResponse(reply, await executePipeline(steps, pipeCtx, (ctx) => controllerMethod(ctx), operation), req);
|
|
288
290
|
};
|
|
289
291
|
}
|
|
290
292
|
/**
|
|
@@ -309,11 +311,12 @@ function buildActionPipelineHandler(handler, steps, operation, resourceName) {
|
|
|
309
311
|
data: await handler(id, data, req)
|
|
310
312
|
});
|
|
311
313
|
return async (id, data, req) => {
|
|
312
|
-
|
|
314
|
+
const pipeCtx = {
|
|
313
315
|
...createRequestContext(req),
|
|
314
316
|
resource: resourceName,
|
|
315
317
|
operation
|
|
316
|
-
}
|
|
318
|
+
};
|
|
319
|
+
return executePipeline(steps, pipeCtx, async (_ctx) => ({
|
|
317
320
|
status: 200,
|
|
318
321
|
data: await handler(id, data, req)
|
|
319
322
|
}), operation);
|
package/dist/plugins/index.d.mts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { t as
|
|
1
|
+
|
|
2
|
+
import { At as RouteDefinition, J as PresetHook, Q as MiddlewareConfig, et as RouteSchemaOptions } from "../index-CpzxqePp.mjs";
|
|
3
|
+
import { l as AnyRecord } from "../types-B7XeLEgK.mjs";
|
|
4
|
+
import { i as errorHandlerPlugin, n as ErrorMapper, r as defaultIsDuplicateKeyError, t as ErrorHandlerOptions } from "../errorHandler-CpYUJmPA.mjs";
|
|
5
|
+
import { _ as HealthCheck, b as healthPlugin, f as ScheduleDefinition, g as schedulesPlugin, h as _default$7, m as SchedulesPluginOptions, p as ScheduleStats, v as HealthOptions, y as _default$3 } from "../types-CT5vu6M0.mjs";
|
|
6
|
+
import { a as arcCorePlugin, i as _default, n as ArcCorePluginOptions, r as PluginMeta, t as ArcCore } from "../fastify-augmentation-BppG5bMT.mjs";
|
|
7
|
+
import { _ as CachingRule, a as MetricsCollector, c as metricsPlugin, d as _default$5, f as realtimePlugin, g as CachingOptions, h as ssePlugin, i as MetricEntry, l as ChangeOperation, m as _default$8, n as _default$9, o as MetricsOptions, p as SSEOptions, r as versioningPlugin, s as _default$4, t as VersioningOptions, u as RealtimeOptions, v as _default$1, y as cachingPlugin } from "../versioning-D_rhHfZm.mjs";
|
|
8
|
+
import { t as TracingOptions } from "../tracing-Dro5eH6j.mjs";
|
|
8
9
|
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
9
10
|
import { FastifyInstance, FastifyPluginAsync } from "fastify";
|
|
10
11
|
//#region src/plugins/createPlugin.d.ts
|
package/dist/plugins/index.mjs
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { t as
|
|
6
|
-
import { t as
|
|
7
|
-
import {
|
|
8
|
-
import { n as
|
|
9
|
-
import { n as
|
|
10
|
-
import { n as
|
|
11
|
-
import { n as
|
|
12
|
-
import { t as
|
|
13
|
-
import {
|
|
14
|
-
import { n as
|
|
15
|
-
import { n as
|
|
1
|
+
/*! @classytic/arc v2.32.0 | MIT | https://classytic.com */
|
|
2
|
+
import { p as MUTATION_OPERATIONS } from "../constants-J99LGTJI.mjs";
|
|
3
|
+
import { c as getOrgId } from "../types-DXYX7dIR.mjs";
|
|
4
|
+
import { n as requestContext } from "../requestContext-B6vQF-sy.mjs";
|
|
5
|
+
import { t as hasEvents } from "../typeGuards-B_5-hKjJ.mjs";
|
|
6
|
+
import { t as HookSystem } from "../HookSystem-vimw5D7Z.mjs";
|
|
7
|
+
import { t as ResourceRegistry } from "../ResourceRegistry-BVr7PgA0.mjs";
|
|
8
|
+
import { a as requestIdPlugin, i as createRequestIdGenerator, n as health_default, o as requestId_default, s as sanitizeRequestId, t as healthPlugin } from "../health-5XnTa2uo.mjs";
|
|
9
|
+
import { n as caching_default, t as cachingPlugin } from "../caching-DFJqHyFs.mjs";
|
|
10
|
+
import { n as errorHandlerPlugin, t as defaultIsDuplicateKeyError } from "../errorHandler-B2AZUJUj.mjs";
|
|
11
|
+
import { n as metrics_default, t as metricsPlugin } from "../metrics-DdQL8syd.mjs";
|
|
12
|
+
import { n as realtime_default, t as realtimePlugin } from "../realtime-DVvVZZFz.mjs";
|
|
13
|
+
import { t as replyHelpersPlugin } from "../replyHelpers-DS-P0b32.mjs";
|
|
14
|
+
import { n as schedules_default, t as schedulesPlugin } from "../schedules-C2Q3uv2d.mjs";
|
|
15
|
+
import { n as sse_default, t as ssePlugin } from "../sse-B9P4Y-UI.mjs";
|
|
16
|
+
import { n as versioning_default, t as versioningPlugin } from "../versioning-CKv2jz_U.mjs";
|
|
16
17
|
import fp from "fastify-plugin";
|
|
17
18
|
//#region src/core/arcCorePlugin.ts
|
|
18
19
|
const arcCorePlugin = async (fastify, opts = {}) => {
|