@classytic/arc 2.41.0 → 2.43.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/dist/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
- package/dist/{EventTransport-n7qO5Cz3.mjs → EventTransport-tzA7CAB4.mjs} +22 -17
- package/dist/{HookSystem-CxvZBo77.mjs → HookSystem-B6UaKRbF.mjs} +2 -2
- package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
- package/dist/{ResourceRegistry-Cu7uTJp9.mjs → ResourceRegistry-CuCARu2B.mjs} +44 -13
- package/dist/actionPermissions-BotX1YOa.mjs +776 -0
- package/dist/audit/index.d.mts +43 -5
- package/dist/audit/index.mjs +48 -26
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +43 -13
- package/dist/auth/index.mjs +130 -155
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-2YY_Kaj1.mjs → authorizationDecision-DQYrkl5f.mjs} +7 -102
- package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
- package/dist/boundedMap-BULzfXYU.mjs +57 -0
- package/dist/{buildHandler-CUhjGHGZ.mjs → buildHandler-CRQ8EcX5.mjs} +4 -4
- package/dist/cache/index.d.mts +22 -34
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-DsWDjIxx.mjs → caching-Cc2M1DwO.mjs} +3 -3
- package/dist/claimed-B10RTfkM.mjs +8 -0
- package/dist/cleanup/index.d.mts +6 -10
- package/dist/cleanup/index.mjs +27 -73
- package/dist/cli/commands/describe.d.mts +4 -2
- package/dist/cli/commands/describe.mjs +7 -4
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -2
- package/dist/cli/commands/init.mjs +2 -2
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-BxLgNFF7.mjs → constants-Cu7Mx7_U.mjs} +4 -2
- package/dist/context/index.d.mts +2 -5
- package/dist/context/index.mjs +18 -38
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +8 -8
- package/dist/{core-iNGqYerO.mjs → core-Cb0LjccA.mjs} +5 -4
- package/dist/{createActionRouter-BBlbPs3k.mjs → createActionRouter-xjqonMRO.mjs} +87 -39
- package/dist/{createAggregationRouter-Dv5SuK2x.mjs → createAggregationRouter-zRYWw_5D.mjs} +11 -8
- package/dist/{preloadResources-Bbp4QJ8V.mjs → createApp-D-953ZAq.mjs} +299 -390
- package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-C9cxS8zt.d.mts} +1 -1
- package/dist/{defineAggregation-UyeurGkp.mjs → defineAggregation-Bm8RjMbq.mjs} +1 -1
- package/dist/{defineResource-DzGT-2LP.mjs → defineResource-DIC7JZGU.mjs} +118 -1024
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +10 -4
- package/dist/docs/index.mjs +4 -4
- package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
- package/dist/{elevation-BF-vhGxY.mjs → elevation-DG8SrLN-.mjs} +41 -12
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-CIMkCOWm.mjs → errorHandler-BnfDviL1.mjs} +26 -5
- package/dist/{errors-C_YhELhe.mjs → errors-CtrywMHP.mjs} +58 -23
- package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
- package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
- package/dist/{eventPlugin-CmXaPWLv.mjs → eventPlugin-Q6BhzHzs.mjs} +82 -21
- package/dist/{subscribe-helpers-AVYPgqTl.mjs → eventTypes-Cdhm6ZlV.mjs} +151 -17
- package/dist/events/index.d.mts +33 -8
- package/dist/events/index.mjs +49 -53
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +139 -157
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +2 -2
- package/dist/factory/index.d.mts +32 -17
- package/dist/factory/index.mjs +213 -32
- package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DhUJ-mvo.d.mts} +11 -25
- package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
- package/dist/{fields-BTyPLHK0.mjs → fields-ToPYpMqS.mjs} +1 -1
- package/dist/filter-merge-IpGywvym.mjs +99 -0
- package/dist/{generate-D_YmiCNf.mjs → generate-DRTkLlIl.mjs} +2 -2
- package/dist/{headers-CNECDE6q.mjs → headers-BbkIF6vA.mjs} +2 -2
- package/dist/health-D-Ft6JM6.mjs +22 -0
- package/dist/{health-CxQzbJmr.mjs → health-DqbsG-_w.mjs} +115 -55
- package/dist/hooks/index.d.mts +2 -2
- package/dist/hooks/index.mjs +2 -2
- package/dist/idempotency/index.d.mts +31 -7
- package/dist/idempotency/index.mjs +155 -98
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +48 -11
- package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
- package/dist/{preloadResources-CjS2OfpU.d.mts → index-C8zVGYsk.d.mts} +37 -38
- package/dist/{index-D4u76eQW.d.mts → index-CRoPtISf.d.mts} +23 -73
- package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
- package/dist/index-JdbZAIsm.d.mts +510 -0
- package/dist/{index-DHbyPo5j.d.mts → index-mqaqZNpe.d.mts} +253 -254
- package/dist/index.d.mts +8 -8
- package/dist/index.mjs +11 -11
- package/dist/{init-Y2MlSN9n.mjs → init-B_oUYDXI.mjs} +1 -1
- package/dist/integrations/event-gateway.d.mts +8 -3
- package/dist/integrations/event-gateway.mjs +4 -4
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs/index.d.mts +1 -1
- package/dist/integrations/jobs/index.mjs +18 -5
- package/dist/integrations/mcp/index.d.mts +72 -59
- package/dist/integrations/mcp/index.mjs +19 -4
- package/dist/integrations/mcp/testing.d.mts +2 -2
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +78 -136
- package/dist/integrations/streamline.mjs +116 -54
- package/dist/integrations/webhooks.d.mts +60 -120
- package/dist/integrations/webhooks.mjs +36 -80
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket-redis.mjs +1 -1
- package/dist/integrations/websocket.d.mts +3 -3
- package/dist/integrations/websocket.mjs +2 -2
- package/dist/{interface-DEWNOMza.d.mts → interface-C0395yUN.d.mts} +13 -1
- package/dist/{jsonBody-Cq9KYl2Z.mjs → jsonBody-B4YVGcql.mjs} +2 -2
- package/dist/{keys-Cmv4cBRa.mjs → keys-B2KtgMLJ.mjs} +7 -18
- package/dist/{loadResources-CblF7xt2.mjs → loadResources-Dn4lalw2.mjs} +3 -3
- package/dist/{loadResourcesFromEntry-99fIMC3p.mjs → loadResourcesFromEntry-3sLYbuse.mjs} +2 -2
- package/dist/logger/index.d.mts +1 -8
- package/dist/logger/index.mjs +3 -153
- package/dist/logger-DXjfIqjV.mjs +160 -0
- package/dist/{mcpPlugin-xF3Gw-HW.mjs → mcpPlugin-C08HalN5.mjs} +433 -170
- package/dist/{memory-CpDZ9NPw.mjs → memory-9Cp7611C.mjs} +36 -34
- package/dist/{metrics-BaKnq8ag.mjs → metrics-CWI11eRu.mjs} +39 -72
- package/dist/middleware/index.d.mts +2 -2
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.mjs +2 -2
- package/dist/{module-BkQEesNQ.mjs → module-BSvOQC7E.mjs} +8 -8
- package/dist/multiTenant-cY0FkDiA.d.mts +104 -0
- package/dist/{multipartBody-C92aJ8am.mjs → multipartBody-CXM2a-Dy.mjs} +46 -7
- package/dist/{openapi-C3LFRzPq.mjs → openapi-DT_G4VhY.mjs} +48 -10
- package/dist/outbox-admin/index.d.mts +3 -3
- package/dist/outbox-admin/index.mjs +6 -18
- package/dist/{outbox-module-dsCyLxXH.mjs → outbox-module-Bjgzl6tg.mjs} +203 -33
- package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
- package/dist/permissions/index.d.mts +5 -4
- package/dist/permissions/index.mjs +8 -6
- package/dist/{permissions-DiHXy8Ck.mjs → permissions-Cm0uYHvd.mjs} +149 -33
- package/dist/{pipe-CBVmUJ-j.mjs → pipe-BqeGT6UO.mjs} +2 -2
- package/dist/pipeline/index.d.mts +2 -2
- package/dist/pipeline/index.mjs +2 -2
- package/dist/plan-FVR6LP59.mjs +565 -0
- package/dist/{BaseController-DYJRZ0EJ.mjs → plugin-BccET1tL.mjs} +1256 -344
- package/dist/plugins/index.d.mts +14 -11
- package/dist/plugins/index.mjs +44 -22
- package/dist/plugins/response-cache.mjs +8 -14
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-2_U6QZQg.mjs → pluralize-CVIOcgMF.mjs} +1 -1
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +26 -31
- package/dist/presets/index.mjs +4 -4
- package/dist/presets/multiTenant.d.mts +2 -107
- package/dist/presets/multiTenant.mjs +22 -162
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-BdlugYTc.mjs → presets-Cpvj6fGO.mjs} +99 -107
- package/dist/{projection-Uvgp-EEj.mjs → projection-Ce4aQhQk.mjs} +2 -2
- package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
- package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
- package/dist/{queryCachePlugin-B5F09bYr.mjs → queryCachePlugin-CFWNH8aT.mjs} +12 -14
- package/dist/{rateLimitKey-CK-EU2L8.mjs → rateLimitKey-C4O-kPm0.mjs} +1 -1
- package/dist/{realtime-FnnD0n3Z.mjs → realtime-DmPE9S54.mjs} +25 -21
- package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
- package/dist/{redis-Bzi1y3F9.d.mts → redis-vLvtcGy_.d.mts} +13 -24
- package/dist/redisShapes-CQksY67q.d.mts +55 -0
- package/dist/registry/index.d.mts +9 -4
- package/dist/registry/index.mjs +4 -4
- package/dist/{registry-D3y8VEJ1.mjs → registry-Bcz9G1bh.mjs} +10 -8
- package/dist/{renewingLease-B3VYh5aU.mjs → renewingLease-qODa7KJQ.mjs} +1 -1
- package/dist/{replyHelpers-DPHsdw-5.mjs → replyHelpers-CG_VtVgS.mjs} +2 -2
- package/dist/{requestContext-cSVKnYPk.mjs → requestContext-Dsu9P1bV.mjs} +1 -1
- package/dist/requirement-CbiLdQ1n.d.mts +22 -0
- package/dist/requirement-KDdoRcOP.mjs +35 -0
- package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
- package/dist/resourceRateLimit-dSjVr1pk.mjs +180 -0
- package/dist/{rolldown-runtime-DjDuyglE.mjs → rolldown-runtime-D1w9Y1VJ.mjs} +1 -1
- package/dist/routeValidator-D36SW8qt.mjs +70 -0
- package/dist/{runtimeCapabilities-DSA_dH1_.mjs → runtimeCapabilities-0tccjRlj.mjs} +1 -1
- package/dist/{schedules-BktJb3Y3.mjs → schedules-C-OpmHBq.mjs} +46 -37
- package/dist/{schemaConverter-BLCSmNbr.mjs → schemaConverter-BORek3O-.mjs} +2 -2
- package/dist/{schemaIR-K16Bb5BX.mjs → schemaIR-C7Vbp71I.mjs} +2 -2
- package/dist/schemas/index.d.mts +10 -0
- package/dist/schemas/index.mjs +8 -3
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +6 -6
- package/dist/{scope-jSxuBTFM.mjs → scope-DQthe88b.mjs} +4 -4
- package/dist/shutdown-MqA81z_P.mjs +59 -0
- package/dist/{simpleEqualityMatcher-BiOnyflP.mjs → simpleEqualityMatcher-auW8QjXN.mjs} +1 -1
- package/dist/{sse-CH7Slxc1.mjs → sse-CYzQBVFe.mjs} +11 -15
- package/dist/{sseStream-CN-Lh1Tu.mjs → sseStream-B5_Afiqn.mjs} +2 -2
- package/dist/{store-helpers-9DX9-1j_.mjs → store-helpers-BOzk-CuX.mjs} +1 -1
- package/dist/{streaming-dvppnxsr.mjs → streaming-DjTQhZrN.mjs} +1 -1
- package/dist/subscriberIdentity-CW39uA8G.mjs +28 -0
- package/dist/tenancy--ed9eH_z.mjs +43 -0
- package/dist/tenantPreparation-BKon6Yox.mjs +67 -0
- package/dist/testing/cleanupStoreContract.mjs +1 -1
- package/dist/testing/index.d.mts +193 -175
- package/dist/testing/index.mjs +286 -212
- package/dist/testing/outboxStoreContract.mjs +137 -2
- package/dist/testing/requiredPorts.d.mts +17 -0
- package/dist/testing/requiredPorts.mjs +65 -0
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/timing-qczZ0YC-.mjs +46 -0
- package/dist/{transactionContext-DCWMI6hV.mjs → transactionContext-DjFkrNG6.mjs} +1 -1
- package/dist/{typeGuards-D7tHFdH6.mjs → typeGuards-CySF12G-.mjs} +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/index.mjs +1 -1
- package/dist/types/storage.mjs +1 -1
- package/dist/{types-CWQn9-IC.d.mts → types-B4Aq4Sdj.d.mts} +22 -9
- package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
- package/dist/{types-CDy3-zw1.mjs → types-Bk9NOgOC.mjs} +4 -4
- package/dist/usage/index.d.mts +2 -3
- package/dist/usage/index.mjs +5 -5
- package/dist/{userHelpers-DDMOZ_Pm.mjs → userHelpers-aklawRaK.mjs} +1 -1
- package/dist/utils/index.d.mts +84 -223
- package/dist/utils/index.mjs +13 -12
- package/dist/{utils-B-zj_b__.mjs → utils-ec1oTo1n.mjs} +315 -404
- package/dist/{validate-R0G_JrnK.mjs → validate-9yzHC1e2.mjs} +6 -14
- package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
- package/dist/{versioning-DuVWcu-F.mjs → versioning-fhhWHV74.mjs} +4 -4
- package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
- package/dist/{workScope-BhsWde4k.mjs → workScope-zpPyge4t.mjs} +2 -2
- package/package.json +578 -574
- package/skills/arc/SKILL.md +13 -4
- package/skills/arc/references/auth.md +8 -5
- package/skills/arc/references/integrations.md +1 -1
- package/skills/arc/references/mcp.md +8 -6
- package/skills/arc/references/multi-tenancy.md +19 -14
- package/skills/arc/references/production.md +27 -10
- package/dist/actionPermissions-DPwPWPOV.mjs +0 -350
- package/dist/index-CFfLKMH4.d.mts +0 -290
- package/dist/interface-BIpgzbZl.mjs +0 -17
- package/dist/redis-stream-BPKscsTB.d.mts +0 -260
- package/dist/registerRoute-DRsSHPSM.mjs +0 -364
- package/dist/sync/changelog.d.mts +0 -3
- package/dist/sync/changelog.mjs +0 -3
package/dist/auth/index.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-
|
|
3
|
-
import { n as requireSingleHeaderValue } from "../headers-
|
|
4
|
-
import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-
|
|
5
|
-
import { o as toJsonSchema } from "../schemaConverter-
|
|
6
|
-
import { n as declareRuntimeCapability } from "../runtimeCapabilities-
|
|
7
|
-
import { E as requireOrgRole, T as requireOrgMembership, k as requireTeamMembership } from "../permissions-
|
|
1
|
+
/*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
|
|
2
|
+
import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-CtrywMHP.mjs";
|
|
3
|
+
import { n as requireSingleHeaderValue } from "../headers-BbkIF6vA.mjs";
|
|
4
|
+
import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-aklawRaK.mjs";
|
|
5
|
+
import { o as toJsonSchema } from "../schemaConverter-BORek3O-.mjs";
|
|
6
|
+
import { n as declareRuntimeCapability } from "../runtimeCapabilities-0tccjRlj.mjs";
|
|
7
|
+
import { E as requireOrgRole, T as requireOrgMembership, k as requireTeamMembership } from "../permissions-Cm0uYHvd.mjs";
|
|
8
|
+
import { ARC_ERROR_CODES } from "@classytic/repo-core/errors";
|
|
8
9
|
import { isProductionEnv } from "@classytic/primitives/environment";
|
|
9
10
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
10
11
|
import fp from "fastify-plugin";
|
|
@@ -543,6 +544,22 @@ function mergeUserFieldsIntoRequestBody(requestBody, userFields, endpointPath) {
|
|
|
543
544
|
//#endregion
|
|
544
545
|
//#region src/auth/betterAuth.ts
|
|
545
546
|
/**
|
|
547
|
+
* Better Auth Adapter for Arc/Fastify
|
|
548
|
+
*
|
|
549
|
+
* Bridges Fastify <-> Better Auth's Fetch API (Request/Response).
|
|
550
|
+
* Better Auth is the USER's dependency -- Arc only provides this thin adapter.
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* import { betterAuth } from 'better-auth';
|
|
554
|
+
* import { createBetterAuthAdapter } from '@classytic/arc/auth';
|
|
555
|
+
*
|
|
556
|
+
* const auth = betterAuth({ ... });
|
|
557
|
+
*
|
|
558
|
+
* const app = await createApp({
|
|
559
|
+
* auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth }) },
|
|
560
|
+
* });
|
|
561
|
+
*/
|
|
562
|
+
/**
|
|
546
563
|
* Convert a Fastify request into a Fetch API Request.
|
|
547
564
|
*
|
|
548
565
|
* Better Auth expects standard Web API Request objects.
|
|
@@ -604,28 +621,23 @@ async function sendFetchResponse(response, reply) {
|
|
|
604
621
|
}
|
|
605
622
|
}
|
|
606
623
|
/**
|
|
607
|
-
* Resolve the current session via
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
* absent — this surfaces immediately and clearly when an integrator passes a
|
|
611
|
-
* stub handler instead of a real `betterAuth()` instance.
|
|
624
|
+
* Resolve the current session via `auth.api.getSession`. Throws
|
|
625
|
+
* `ArcError(BETTER_AUTH_API_MISSING)` when it is absent (a stub, not a real
|
|
626
|
+
* `betterAuth()` instance).
|
|
612
627
|
*/
|
|
613
628
|
async function getSessionDirect(auth, headers) {
|
|
614
629
|
const api = auth.api;
|
|
615
|
-
if (!api || typeof api.getSession !== "function") throw new ArcError("Better Auth instance is missing `api.getSession` — arc
|
|
616
|
-
code:
|
|
630
|
+
if (!api || typeof api.getSession !== "function") throw new ArcError("Better Auth instance is missing `api.getSession` — arc requires the in-process API map. Pass a real `betterAuth()` instance or supply an `api: { getSession }` stub.", {
|
|
631
|
+
code: ARC_ERROR_CODES.AUTH_MISCONFIGURED,
|
|
617
632
|
statusCode: 500
|
|
618
633
|
});
|
|
619
634
|
const result = await api.getSession({ headers });
|
|
620
635
|
return result?.user ? result : null;
|
|
621
636
|
}
|
|
622
637
|
/**
|
|
623
|
-
* Read a method from `auth.api
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
* Real Better Auth 1.6.x flattens every plugin endpoint onto the top-level
|
|
628
|
-
* `api` object. The nested form is kept as a fallback for hand-rolled stubs.
|
|
638
|
+
* Read a method from `auth.api`: the flat shape real instances expose
|
|
639
|
+
* (`api.getActiveMember`), falling back to the nested stub shape
|
|
640
|
+
* (`api.organization.getActiveMember`).
|
|
629
641
|
*/
|
|
630
642
|
function pickApiMethod(auth, name, group) {
|
|
631
643
|
const api = auth.api;
|
|
@@ -649,11 +661,8 @@ async function getActiveMemberRoles(auth, headers) {
|
|
|
649
661
|
}
|
|
650
662
|
}
|
|
651
663
|
/**
|
|
652
|
-
* Resolve org roles for an explicit organizationId
|
|
653
|
-
*
|
|
654
|
-
* Required for API key auth where the synthetic session lacks
|
|
655
|
-
* `activeOrganizationId` — callers pass org context via the
|
|
656
|
-
* `x-organization-id` header.
|
|
664
|
+
* Resolve org roles for an explicit organizationId — for sessions without
|
|
665
|
+
* `activeOrganizationId` (e.g. API-key auth via `x-organization-id`).
|
|
657
666
|
*/
|
|
658
667
|
async function getMemberRolesByOrg(auth, headers, organizationId) {
|
|
659
668
|
const fn = pickApiMethod(auth, "getActiveMemberRole", "organization");
|
|
@@ -669,21 +678,12 @@ async function getMemberRolesByOrg(auth, headers, organizationId) {
|
|
|
669
678
|
}
|
|
670
679
|
}
|
|
671
680
|
/**
|
|
672
|
-
* Teams the current user is a MEMBER of
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
* `auth.api.listUserTeams` (`/organization/list-user-teams`) is the only name
|
|
676
|
-
* probed. A `listTeams` fallback lived here for "older 1.5.x" and was removed:
|
|
677
|
-
* Better Auth exposes `listUserTeams` and `listOrganizationTeams` on `auth.api`
|
|
678
|
-
* and has never exposed a bare `listTeams` — that is the CLIENT name
|
|
679
|
-
* (`authClient.organization.listTeams`). It could only ever resolve against a
|
|
680
|
-
* test double, which is exactly what it was doing.
|
|
681
|
+
* Teams the current user is a MEMBER of in one organization, used to validate
|
|
682
|
+
* `activeTeamId` before binding it to scope.
|
|
681
683
|
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
* user is not a member of — the organization check below constrains the org, not
|
|
686
|
-
* membership.
|
|
684
|
+
* Must stay `listUserTeams`: `listOrganizationTeams` lists EVERY team in the
|
|
685
|
+
* org and would admit a team the user isn't a member of. (`listTeams` is the
|
|
686
|
+
* client-side name; `auth.api` has no such method.)
|
|
687
687
|
*/
|
|
688
688
|
async function listTeamsDirect(auth, headers, organizationId) {
|
|
689
689
|
const fn = pickApiMethod(auth, "listUserTeams", "organization");
|
|
@@ -737,38 +737,72 @@ async function resolveActiveTeamId(input) {
|
|
|
737
737
|
if (!activeTeamId) return void 0;
|
|
738
738
|
return (await listTeamsDirect(input.auth, input.headers, input.activeOrgId))?.find((team) => normalizeId(team.id ?? team._id) === activeTeamId && normalizeId(team.organizationId) === input.activeOrgId) ? activeTeamId : void 0;
|
|
739
739
|
}
|
|
740
|
-
|
|
741
|
-
* Create a Better Auth adapter for Arc/Fastify.
|
|
742
|
-
*
|
|
743
|
-
* Returns a Fastify plugin (registers catch-all auth routes) and an
|
|
744
|
-
* `authenticate` preHandler that validates sessions via Better Auth.
|
|
745
|
-
*
|
|
746
|
-
* @example
|
|
747
|
-
* ```typescript
|
|
748
|
-
* import { betterAuth } from 'better-auth';
|
|
749
|
-
* import { createBetterAuthAdapter } from '@classytic/arc/auth';
|
|
750
|
-
*
|
|
751
|
-
* const auth = betterAuth({
|
|
752
|
-
* database: ...,
|
|
753
|
-
* emailAndPassword: { enabled: true },
|
|
754
|
-
* });
|
|
755
|
-
*
|
|
756
|
-
* const { plugin, authenticate } = createBetterAuthAdapter({ auth });
|
|
757
|
-
*
|
|
758
|
-
* // Register the plugin (catch-all auth routes)
|
|
759
|
-
* await fastify.register(plugin);
|
|
760
|
-
*
|
|
761
|
-
* // Use authenticate as a preHandler on protected routes
|
|
762
|
-
* fastify.get('/me', { preHandler: [authenticate] }, handler);
|
|
763
|
-
* ```
|
|
764
|
-
*/
|
|
740
|
+
const AUTH_SCHEME = /^[A-Za-z][A-Za-z0-9!#$%&'*+.^_`|~-]*$/;
|
|
765
741
|
function createBetterAuthAdapter(options) {
|
|
766
|
-
const { auth, basePath = "/api/auth", orgContext: orgContextOpt = false, openapi: openapiOpt = true, userFields, exposeAuthErrors = false } = options;
|
|
742
|
+
const { auth, basePath = "/api/auth", orgContext: orgContextOpt = false, openapi: openapiOpt = true, userFields, exposeAuthErrors = false, resolveOrgRoles, memberLookup = "better-auth", credentials = {} } = options;
|
|
743
|
+
const verifiers = /* @__PURE__ */ new Map();
|
|
744
|
+
for (const [scheme, verify] of Object.entries(credentials)) {
|
|
745
|
+
if (!AUTH_SCHEME.test(scheme)) throw new Error(`createBetterAuthAdapter: "${scheme}" is not an auth scheme name.`);
|
|
746
|
+
if (scheme.toLowerCase() === "bearer") throw new Error("createBetterAuthAdapter: the Bearer scheme is the session's.");
|
|
747
|
+
verifiers.set(scheme.toLowerCase(), verify);
|
|
748
|
+
}
|
|
749
|
+
/** A registered non-session credential on this request, if any. */
|
|
750
|
+
const presentedCredential = (request) => {
|
|
751
|
+
if (verifiers.size === 0) return null;
|
|
752
|
+
const header = request.headers.authorization;
|
|
753
|
+
if (typeof header !== "string") return null;
|
|
754
|
+
const space = header.indexOf(" ");
|
|
755
|
+
if (space <= 0) return null;
|
|
756
|
+
const verify = verifiers.get(header.slice(0, space).toLowerCase());
|
|
757
|
+
return verify ? {
|
|
758
|
+
verify,
|
|
759
|
+
value: header.slice(space + 1).trim()
|
|
760
|
+
} : null;
|
|
761
|
+
};
|
|
767
762
|
const normalizedBase = basePath.replace(/\/+$/, "");
|
|
768
763
|
const orgEnabled = !!orgContextOpt;
|
|
764
|
+
if (resolveOrgRoles && !orgEnabled) throw new Error("createBetterAuthAdapter: `resolveOrgRoles` requires `orgContext: true`.");
|
|
765
|
+
if (memberLookup !== "better-auth" && memberLookup !== "resolver") throw new Error(`createBetterAuthAdapter: \`memberLookup\` must be 'better-auth' or 'resolver' (got ${String(memberLookup)}).`);
|
|
766
|
+
if (memberLookup === "resolver" && !resolveOrgRoles) throw new Error("createBetterAuthAdapter: `memberLookup: 'resolver'` requires `resolveOrgRoles` — without it nothing would read membership.");
|
|
767
|
+
const skipMemberLookup = memberLookup === "resolver";
|
|
768
|
+
/**
|
|
769
|
+
* The member scope for the active organization (session's active org wins; the header covers
|
|
770
|
+
* sessions without one), or `undefined` when the caller holds no role there. ONE path for
|
|
771
|
+
* `authenticate` and `optionalAuthenticate`, so `resolveOrgRoles` cannot apply on one and not the
|
|
772
|
+
* other.
|
|
773
|
+
*/
|
|
774
|
+
const resolveMemberScope = async (headers, session, requestedOrgId, userId, userRoles) => {
|
|
775
|
+
const activeOrgId = session?.activeOrganizationId || requestedOrgId;
|
|
776
|
+
if (!activeOrgId) return void 0;
|
|
777
|
+
let orgRoles = null;
|
|
778
|
+
if (!skipMemberLookup) {
|
|
779
|
+
if (session?.activeOrganizationId) orgRoles = await getActiveMemberRoles(auth, headers);
|
|
780
|
+
if (!orgRoles) orgRoles = await getMemberRolesByOrg(auth, headers, activeOrgId);
|
|
781
|
+
}
|
|
782
|
+
if (resolveOrgRoles && userId) orgRoles = await resolveOrgRoles({
|
|
783
|
+
userId,
|
|
784
|
+
organizationId: activeOrgId,
|
|
785
|
+
memberRoles: orgRoles
|
|
786
|
+
});
|
|
787
|
+
if (!orgRoles) return void 0;
|
|
788
|
+
const scope = {
|
|
789
|
+
kind: "member",
|
|
790
|
+
userId,
|
|
791
|
+
userRoles,
|
|
792
|
+
organizationId: activeOrgId,
|
|
793
|
+
orgRoles: [...orgRoles]
|
|
794
|
+
};
|
|
795
|
+
const activeTeamId = await resolveActiveTeamId({
|
|
796
|
+
auth,
|
|
797
|
+
headers,
|
|
798
|
+
session,
|
|
799
|
+
activeOrgId
|
|
800
|
+
});
|
|
801
|
+
if (activeTeamId) scope.teamId = activeTeamId;
|
|
802
|
+
return scope;
|
|
803
|
+
};
|
|
769
804
|
/**
|
|
770
|
-
* Validates the
|
|
771
|
-
* to Better Auth's `GET /api/auth/get-session` endpoint.
|
|
805
|
+
* Validates the session via `auth.api.getSession` with the request headers.
|
|
772
806
|
*
|
|
773
807
|
* On success, sets `request.user` and `request.session`.
|
|
774
808
|
* When orgContext is enabled, also sets `request.scope` to
|
|
@@ -776,12 +810,26 @@ function createBetterAuthAdapter(options) {
|
|
|
776
810
|
* On failure, replies with 401.
|
|
777
811
|
*/
|
|
778
812
|
const authenticate = async (request, reply) => {
|
|
813
|
+
const presented = presentedCredential(request);
|
|
814
|
+
if (presented) {
|
|
815
|
+
const scope = await presented.verify(presented.value, request);
|
|
816
|
+
if (!scope) {
|
|
817
|
+
reply.code(401).send({
|
|
818
|
+
code: ARC_ERROR_CODES.UNAUTHORIZED,
|
|
819
|
+
message: "Invalid credentials",
|
|
820
|
+
status: 401
|
|
821
|
+
});
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
request.scope = scope;
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
779
827
|
try {
|
|
780
828
|
const headers = buildHeaders(request);
|
|
781
829
|
const sessionData = await getSessionDirect(auth, headers);
|
|
782
830
|
if (!sessionData?.user) {
|
|
783
831
|
reply.code(401).send({
|
|
784
|
-
code:
|
|
832
|
+
code: ARC_ERROR_CODES.UNAUTHORIZED,
|
|
785
833
|
message: "Invalid or expired session",
|
|
786
834
|
status: 401
|
|
787
835
|
});
|
|
@@ -799,47 +847,14 @@ function createBetterAuthAdapter(options) {
|
|
|
799
847
|
userRoles
|
|
800
848
|
};
|
|
801
849
|
if (orgEnabled) {
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
const activeOrgId = session?.activeOrganizationId || requestedOrgId;
|
|
805
|
-
if (activeOrgId) {
|
|
806
|
-
/**
|
|
807
|
-
* `getActiveMember` resolves the member for the SESSION's active
|
|
808
|
-
* organization, so with no `activeOrganizationId` it cannot succeed —
|
|
809
|
-
* it throws `No active organization`, which the helper swallows into
|
|
810
|
-
* `null` after paying the full endpoint round trip.
|
|
811
|
-
*
|
|
812
|
-
* A bearer-only deployment never calls `setActive`, so that was every
|
|
813
|
-
* request: ~440ms spent to learn what the session already said, before
|
|
814
|
-
* the ~870ms header-based lookup that actually answers. Ask the
|
|
815
|
-
* session first; it is already in hand.
|
|
816
|
-
*/
|
|
817
|
-
let orgRoles = session?.activeOrganizationId ? await getActiveMemberRoles(auth, headers) : null;
|
|
818
|
-
if (!orgRoles) orgRoles = await getMemberRolesByOrg(auth, headers, activeOrgId);
|
|
819
|
-
if (orgRoles) {
|
|
820
|
-
const scope = {
|
|
821
|
-
kind: "member",
|
|
822
|
-
userId,
|
|
823
|
-
userRoles,
|
|
824
|
-
organizationId: activeOrgId,
|
|
825
|
-
orgRoles
|
|
826
|
-
};
|
|
827
|
-
const activeTeamId = await resolveActiveTeamId({
|
|
828
|
-
auth,
|
|
829
|
-
headers,
|
|
830
|
-
session,
|
|
831
|
-
activeOrgId
|
|
832
|
-
});
|
|
833
|
-
if (activeTeamId) scope.teamId = activeTeamId;
|
|
834
|
-
req.scope = scope;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
850
|
+
const memberScope = await resolveMemberScope(headers, sessionData.session, requireSingleHeaderValue(request.headers, "x-organization-id"), userId, userRoles);
|
|
851
|
+
if (memberScope) req.scope = memberScope;
|
|
837
852
|
}
|
|
838
853
|
} catch (err) {
|
|
839
854
|
if (err instanceof ArcError) throw err;
|
|
840
855
|
const message = exposeAuthErrors ? err instanceof Error ? err.message : String(err) : "Authentication required";
|
|
841
856
|
reply.code(401).send({
|
|
842
|
-
code:
|
|
857
|
+
code: ARC_ERROR_CODES.UNAUTHORIZED,
|
|
843
858
|
message,
|
|
844
859
|
status: 401
|
|
845
860
|
});
|
|
@@ -854,6 +869,12 @@ function createBetterAuthAdapter(options) {
|
|
|
854
869
|
* flexible filter) can apply org-scoped queries when a user IS authenticated.
|
|
855
870
|
*/
|
|
856
871
|
const optionalAuthenticate = async (request, _reply) => {
|
|
872
|
+
const presented = presentedCredential(request);
|
|
873
|
+
if (presented) {
|
|
874
|
+
const scope = await presented.verify(presented.value, request);
|
|
875
|
+
if (scope) request.scope = scope;
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
857
878
|
try {
|
|
858
879
|
const headers = buildHeaders(request);
|
|
859
880
|
const sessionData = await getSessionDirect(auth, headers);
|
|
@@ -870,41 +891,8 @@ function createBetterAuthAdapter(options) {
|
|
|
870
891
|
userRoles: optUserRoles
|
|
871
892
|
};
|
|
872
893
|
if (orgEnabled) {
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
const activeOrgId = session?.activeOrganizationId || requestedOrgId;
|
|
876
|
-
if (activeOrgId) {
|
|
877
|
-
/**
|
|
878
|
-
* `getActiveMember` resolves the member for the SESSION's active
|
|
879
|
-
* organization, so with no `activeOrganizationId` it cannot succeed —
|
|
880
|
-
* it throws `No active organization`, which the helper swallows into
|
|
881
|
-
* `null` after paying the full endpoint round trip.
|
|
882
|
-
*
|
|
883
|
-
* A bearer-only deployment never calls `setActive`, so that was every
|
|
884
|
-
* request: ~440ms spent to learn what the session already said, before
|
|
885
|
-
* the ~870ms header-based lookup that actually answers. Ask the
|
|
886
|
-
* session first; it is already in hand.
|
|
887
|
-
*/
|
|
888
|
-
let orgRoles = session?.activeOrganizationId ? await getActiveMemberRoles(auth, headers) : null;
|
|
889
|
-
if (!orgRoles) orgRoles = await getMemberRolesByOrg(auth, headers, activeOrgId);
|
|
890
|
-
if (orgRoles) {
|
|
891
|
-
const scope = {
|
|
892
|
-
kind: "member",
|
|
893
|
-
userId: optUserId,
|
|
894
|
-
userRoles: optUserRoles,
|
|
895
|
-
organizationId: activeOrgId,
|
|
896
|
-
orgRoles
|
|
897
|
-
};
|
|
898
|
-
const activeTeamId = await resolveActiveTeamId({
|
|
899
|
-
auth,
|
|
900
|
-
headers,
|
|
901
|
-
session,
|
|
902
|
-
activeOrgId
|
|
903
|
-
});
|
|
904
|
-
if (activeTeamId) scope.teamId = activeTeamId;
|
|
905
|
-
req.scope = scope;
|
|
906
|
-
}
|
|
907
|
-
}
|
|
894
|
+
const memberScope = await resolveMemberScope(headers, sessionData.session, requireSingleHeaderValue(request.headers, "x-organization-id"), optUserId, optUserRoles);
|
|
895
|
+
if (memberScope) req.scope = memberScope;
|
|
908
896
|
}
|
|
909
897
|
} catch (err) {
|
|
910
898
|
if (err instanceof ArcError) throw err;
|
|
@@ -914,22 +902,9 @@ function createBetterAuthAdapter(options) {
|
|
|
914
902
|
if (openapiOpt === false) extractedOpenApi = void 0;
|
|
915
903
|
else if (typeof openapiOpt === "object") extractedOpenApi = openapiOpt;
|
|
916
904
|
const betterAuthPlugin = async (fastify) => {
|
|
917
|
-
/**
|
|
918
|
-
* `orgContext` without the `organization()` plugin is a SILENT
|
|
919
|
-
* misconfiguration, so it fails at boot instead.
|
|
920
|
-
*
|
|
921
|
-
* Arc resolves org membership through `auth.api.getActiveMember` /
|
|
922
|
-
* `getActiveMemberRole`. Absent the plugin those methods do not exist,
|
|
923
|
-
* every lookup returns null, and `request.scope` silently stays
|
|
924
|
-
* `'authenticated'` — so `requireOrgId()` throws 403 on every
|
|
925
|
-
* tenant-scoped route. It fails CLOSED (no data leak), but the symptom
|
|
926
|
-
* points nowhere near the cause: the host sees blanket 403s and no
|
|
927
|
-
* indication that a plugin is missing. One check at registration turns
|
|
928
|
-
* that into a sentence.
|
|
929
|
-
*/
|
|
930
905
|
if (orgEnabled) {
|
|
931
906
|
if (!(pickApiMethod(auth, "getActiveMember", "organization") !== void 0 || pickApiMethod(auth, "getActiveMemberRole", "organization") !== void 0)) throw new ArcError("createBetterAuthAdapter({ orgContext: true }) requires Better Auth's `organization()` plugin — `auth.api.getActiveMember` / `getActiveMemberRole` are missing. Without it arc can never resolve membership, so request.scope stays 'authenticated' and every tenant-scoped route answers 403. Add `organization()` to your betterAuth({ plugins }), or drop `orgContext`.", {
|
|
932
|
-
code:
|
|
907
|
+
code: ARC_ERROR_CODES.AUTH_MISCONFIGURED,
|
|
933
908
|
statusCode: 500
|
|
934
909
|
});
|
|
935
910
|
}
|
|
@@ -940,7 +915,7 @@ function createBetterAuthAdapter(options) {
|
|
|
940
915
|
await sendFetchResponse(await auth.handler(fetchRequest), reply);
|
|
941
916
|
} catch (err) {
|
|
942
917
|
throw new ArcError("Authentication service error", {
|
|
943
|
-
code:
|
|
918
|
+
code: ARC_ERROR_CODES.AUTH_SERVICE_ERROR,
|
|
944
919
|
statusCode: 500,
|
|
945
920
|
cause: err instanceof Error ? err : new Error(String(err))
|
|
946
921
|
});
|
|
@@ -1403,4 +1378,4 @@ function mirrorTrustedOriginsFromCors(options) {
|
|
|
1403
1378
|
return Array.from(/* @__PURE__ */ new Set([canonicalUrl, ...corsOrigins]));
|
|
1404
1379
|
}
|
|
1405
1380
|
//#endregion
|
|
1406
|
-
export { MemorySessionStore, authPlugin_default as authPlugin,
|
|
1381
|
+
export { MemorySessionStore, authPlugin_default as authPlugin, createBetterAuthAdapter, createSessionManager, extractBetterAuthOpenApi, mirrorTrustedOriginsFromCors };
|
|
@@ -1,103 +1,7 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import { arcLog } from "./logger
|
|
3
|
-
import { t as
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Policy-filter composition — logical AND, never last-writer-wins.
|
|
7
|
-
*
|
|
8
|
-
* Row-level policy filters can arrive from several INDEPENDENT sources for one
|
|
9
|
-
* request: a tenant preset, an `ownedByUser` grant, an `allOf(...)` branch, a
|
|
10
|
-
* host permission check. Every one of them is a RESTRICTION the row must
|
|
11
|
-
* satisfy, so composing them is logical AND.
|
|
12
|
-
*
|
|
13
|
-
* The historical `{ ...base, ...incoming }` shallow spread did NOT implement AND:
|
|
14
|
-
* when two sources constrained the SAME key with different values, the later one
|
|
15
|
-
* silently REPLACED the earlier one — widening (or erasing) a restriction another
|
|
16
|
-
* layer deliberately imposed. That is a defense-in-depth defect, not a
|
|
17
|
-
* refinement. "Later can refine" is only sound when the refinement is provably
|
|
18
|
-
* narrower, and a bare overwrite proves nothing.
|
|
19
|
-
*
|
|
20
|
-
* {@link conjoinPolicyFilters} composes with AND semantics while preserving the
|
|
21
|
-
* common case byte-for-byte:
|
|
22
|
-
*
|
|
23
|
-
* - key present on only one side → carried through flat. Non-overlapping
|
|
24
|
-
* filters (the ~99% case) produce exactly the same object the old spread did,
|
|
25
|
-
* so there is ZERO behavior change for them.
|
|
26
|
-
* - same key, deep-equal value → kept once (idempotent — e.g. two checks
|
|
27
|
-
* both pinning the same `organizationId`).
|
|
28
|
-
* - same key, DIFFERENT value → BOTH constraints are preserved under an
|
|
29
|
-
* `$and`, so a row must satisfy both. For flat equality that conjunction is
|
|
30
|
-
* unsatisfiable, so the query returns nothing — the safe, correct outcome for
|
|
31
|
-
* contradictory policies (deny), and the earlier restriction is never dropped.
|
|
32
|
-
*
|
|
33
|
-
* Adapter safety: mongokit / sqlitekit compile `$and` natively at the DB layer,
|
|
34
|
-
* and arc's in-memory `simpleEqualityMatcher` (AccessControl.checkPolicyFilters)
|
|
35
|
-
* already fail-closes on operator-shaped filters. So a produced `$and` is either
|
|
36
|
-
* evaluated correctly by the DB or rejected fail-closed in memory — both safe.
|
|
37
|
-
*
|
|
38
|
-
* This is the pragmatic, adapter-neutral form of the AND semantics. A canonical
|
|
39
|
-
* filter IR in `repo-core` (nested and/or/not nodes each adapter compiles) is the
|
|
40
|
-
* fuller design, but this closes the security defect without one.
|
|
41
|
-
*/
|
|
42
|
-
/**
|
|
43
|
-
* Validate + return a `$and` operand's fragment array. A security-policy
|
|
44
|
-
* normalizer must NEVER silently drop a malformed constraint — a non-array
|
|
45
|
-
* `$and` (e.g. `{ $and: { organizationId: "a" } }`) is a policy bug, so fail
|
|
46
|
-
* loud (fail-closed) rather than discard the restriction it was meant to carry.
|
|
47
|
-
*/
|
|
48
|
-
function coercePolicyAnd(value) {
|
|
49
|
-
if (!Array.isArray(value)) throw new Error(`conjoinPolicyFilters: malformed '$and' — expected an array of filter fragments, got ${value === null ? "null" : typeof value}. A policy filter constraint must not be silently dropped.`);
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
/** Are two flat policy-filter values equivalent for conjunction purposes? */
|
|
53
|
-
function filterValueEqual(a, b) {
|
|
54
|
-
if (Object.is(a, b)) return true;
|
|
55
|
-
if (a !== null && b !== null && typeof a === "object" && typeof b === "object") try {
|
|
56
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Conjoin two policy-filter maps with AND semantics. Never silently overwrites a
|
|
64
|
-
* same-key constraint with a different value — conflicting keys are preserved
|
|
65
|
-
* under `$and`. See the module doc for the full contract.
|
|
66
|
-
*/
|
|
67
|
-
function conjoinPolicyFilters(base, incoming) {
|
|
68
|
-
const baseEmpty = !base || Object.keys(base).length === 0;
|
|
69
|
-
const incomingEmpty = !incoming || Object.keys(incoming).length === 0;
|
|
70
|
-
if (baseEmpty) return incomingEmpty ? {} : { ...incoming };
|
|
71
|
-
if (incomingEmpty) return { ...base };
|
|
72
|
-
const result = {};
|
|
73
|
-
const andParts = [];
|
|
74
|
-
const baseAnd = base.$and;
|
|
75
|
-
if (baseAnd !== void 0) andParts.push(...coercePolicyAnd(baseAnd));
|
|
76
|
-
for (const [key, value] of Object.entries(base)) {
|
|
77
|
-
if (key === "$and") continue;
|
|
78
|
-
result[key] = value;
|
|
79
|
-
}
|
|
80
|
-
for (const [key, value] of Object.entries(incoming)) {
|
|
81
|
-
if (key === "$and") {
|
|
82
|
-
andParts.push(...coercePolicyAnd(value));
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (!(key in result)) {
|
|
86
|
-
result[key] = value;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (filterValueEqual(result[key], value)) continue;
|
|
90
|
-
andParts.push({ [key]: result[key] }, { [key]: value });
|
|
91
|
-
delete result[key];
|
|
92
|
-
}
|
|
93
|
-
if (andParts.length > 0) result.$and = andParts;
|
|
94
|
-
for (const source of [base, incoming]) for (const symbol of Object.getOwnPropertySymbols(source)) {
|
|
95
|
-
const descriptor = Object.getOwnPropertyDescriptor(source, symbol);
|
|
96
|
-
if (descriptor) Object.defineProperty(result, symbol, descriptor);
|
|
97
|
-
}
|
|
98
|
-
return result;
|
|
99
|
-
}
|
|
100
|
-
//#endregion
|
|
1
|
+
/*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as arcLog } from "./logger-DXjfIqjV.mjs";
|
|
3
|
+
import { t as conjoinPolicyFilters } from "./filter-merge-IpGywvym.mjs";
|
|
4
|
+
import { t as ArcError } from "./errors-CtrywMHP.mjs";
|
|
101
5
|
//#region src/permissions/authorizationDecision.ts
|
|
102
6
|
const log = arcLog("permissions:enforce");
|
|
103
7
|
/**
|
|
@@ -194,8 +98,9 @@ async function evaluatePermissionDecision(check, context) {
|
|
|
194
98
|
* This is the enforcement point (PEP) — the ONE place scope + data policy are
|
|
195
99
|
* installed (and the obligation seam lives).
|
|
196
100
|
*/
|
|
197
|
-
function applyAuthorizationDecision(decision,
|
|
101
|
+
function applyAuthorizationDecision(decision, req) {
|
|
198
102
|
if (decision.effect !== "allow") return;
|
|
103
|
+
const request = req;
|
|
199
104
|
if (decision.policy) request._policyFilters = conjoinPolicyFilters(request._policyFilters, decision.policy);
|
|
200
105
|
if (decision.scope) {
|
|
201
106
|
const current = request.scope;
|
|
@@ -267,4 +172,4 @@ async function evaluateAndApplyPermission(check, context, request, reply, opts)
|
|
|
267
172
|
return true;
|
|
268
173
|
}
|
|
269
174
|
//#endregion
|
|
270
|
-
export {
|
|
175
|
+
export { normalizeToDecision as i, evaluateAndApplyPermission as n, evaluatePermissionDecision as r, applyAuthorizationDecision as t };
|
|
@@ -169,6 +169,15 @@ type PermissionCheck<TDoc = Record<string, unknown>> = ((context: PermissionCont
|
|
|
169
169
|
interface PermissionCheckMeta {
|
|
170
170
|
/** Set by allowPublic() — marks the endpoint as publicly accessible */
|
|
171
171
|
_isPublic?: boolean;
|
|
172
|
+
/**
|
|
173
|
+
* Set by `allowPublic({ policy })` — the gate admits EVERYONE, but what they
|
|
174
|
+
* can read or write is narrowed by a row policy. Public and row-filtered are
|
|
175
|
+
* independent facts: `_isPublic` says who may call the endpoint, this says the
|
|
176
|
+
* rows are not all of them. Surfaced as `rowFiltered` by `describePermission`,
|
|
177
|
+
* so a "what is publicly exposed?" audit can tell an open catalogue from a
|
|
178
|
+
* catalogue that shows strangers only its published rows.
|
|
179
|
+
*/
|
|
180
|
+
_rowPolicy?: boolean;
|
|
172
181
|
/**
|
|
173
182
|
* Set by `denyAll()` — nobody passes this gate, ever.
|
|
174
183
|
*
|
|
@@ -184,6 +193,14 @@ interface PermissionCheckMeta {
|
|
|
184
193
|
* and an access audit reports a surface that does not exist.
|
|
185
194
|
*/
|
|
186
195
|
_isDenied?: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* A deployment-static closed door, read LAZILY by `describePermission`: when it returns
|
|
198
|
+
* `true`, nobody passes. Set it on a check whose ENTIRE runtime decision is `!_deniedWhen()`
|
|
199
|
+
* (a tier / feature flag fixed at boot but unknown when the check is built). `allOf` is
|
|
200
|
+
* denied when any child is; `anyOf` when every child is; such a check never taints `allOf`
|
|
201
|
+
* as `conditional`.
|
|
202
|
+
*/
|
|
203
|
+
_deniedWhen?: () => boolean;
|
|
187
204
|
/**
|
|
188
205
|
* Set by requireAuth() — a pure identity gate (must be signed in) that adds NO
|
|
189
206
|
* row-level or environmental condition. Marked so `allOf` does not mistake it
|
|
@@ -297,13 +314,18 @@ declare module "fastify" {
|
|
|
297
314
|
/**
|
|
298
315
|
* This read is deliberately NOT scoped to a tenant — a marketplace listing.
|
|
299
316
|
*
|
|
300
|
-
* Set by `multiTenantPreset({ crossTenant: [...] })
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
* made in one of them is silently re-made by the other three. This flag is how that decision
|
|
304
|
-
* travels. Never set from user input.
|
|
317
|
+
* Set by `multiTenantPreset({ crossTenant: [...] })`, read by `resolveTenancy`
|
|
318
|
+
* (`core/tenancy.ts`) — the one rule every read-scoping layer switches on.
|
|
319
|
+
* Never set from user input.
|
|
305
320
|
*/
|
|
306
321
|
_crossTenantRead?: boolean;
|
|
322
|
+
/**
|
|
323
|
+
* Resolved tenant dimensions (`{ organizationId, teamId, … }`) from
|
|
324
|
+
* `multiTenantPreset`'s multi-field specs. Forwarded to the repository as
|
|
325
|
+
* top-level options by `buildTenantRepoOptions`, for kits whose tenant
|
|
326
|
+
* plugin reads `context.<field>`. Never set from user input.
|
|
327
|
+
*/
|
|
328
|
+
_tenantFields?: Record<string, unknown>;
|
|
307
329
|
}
|
|
308
330
|
}
|
|
309
331
|
type AnyRecord = Record<string, unknown>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
|
|
2
|
+
//#region src/collections/boundedMap.ts
|
|
3
|
+
/**
|
|
4
|
+
* Insertion-order LRU on a plain `Map` — the two moves, written once.
|
|
5
|
+
*
|
|
6
|
+
* A `Map` iterates in insertion order, so "move to most-recent" is
|
|
7
|
+
* delete-then-set and "least-recent" is the first key. Seven places in arc
|
|
8
|
+
* hand-rolled that, and hand-rolled copies drifted into real bugs:
|
|
9
|
+
*
|
|
10
|
+
* - the response cache evicted with `while (size >= maxEntries)` BEFORE
|
|
11
|
+
* inserting, so `maxEntries: 0` spun forever on the first write — no key
|
|
12
|
+
* to evict, loop condition never changes, event loop frozen;
|
|
13
|
+
* - `maxEntries: NaN` made the same comparison always false: unbounded;
|
|
14
|
+
* - the MCP auth cache guarded eviction with `if (firstKey)`, a falsy check,
|
|
15
|
+
* so an empty-string key could never be evicted.
|
|
16
|
+
*
|
|
17
|
+
* These are functions over the caller's own `Map`, not a class, on purpose:
|
|
18
|
+
* each site keeps its TTL, byte accounting and stats, and borrows only the
|
|
19
|
+
* part that kept going wrong.
|
|
20
|
+
*/
|
|
21
|
+
/** Move `key` to most-recent and set it. Use on read-hit (LRU) or on overwrite. */
|
|
22
|
+
function touch(map, key, value) {
|
|
23
|
+
map.delete(key);
|
|
24
|
+
map.set(key, value);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Evict least-recent entries until `map.size <= maxSize`; returns how many.
|
|
28
|
+
*
|
|
29
|
+
* Call AFTER inserting. That ordering is what makes it total: with
|
|
30
|
+
* `maxSize: 0` the just-inserted entry is evicted (a disabled cache), never an
|
|
31
|
+
* infinite loop, and the loop terminates on the iterator running dry rather
|
|
32
|
+
* than on the size comparison alone. A non-finite or negative `maxSize` is a
|
|
33
|
+
* configuration bug — {@link assertCapacity} it where the option is read.
|
|
34
|
+
*/
|
|
35
|
+
function evictToSize(map, maxSize, onEvict) {
|
|
36
|
+
let evicted = 0;
|
|
37
|
+
while (map.size > maxSize) {
|
|
38
|
+
const oldest = map.entries().next();
|
|
39
|
+
if (oldest.done) break;
|
|
40
|
+
const [key, value] = oldest.value;
|
|
41
|
+
map.delete(key);
|
|
42
|
+
onEvict?.(key, value);
|
|
43
|
+
evicted++;
|
|
44
|
+
}
|
|
45
|
+
return evicted;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Validate a capacity option at construction: a non-negative integer. `0` is
|
|
49
|
+
* allowed and means "keep nothing". Throws with the option's name so the error
|
|
50
|
+
* points at the host's config, not at a hang or an unbounded map later.
|
|
51
|
+
*/
|
|
52
|
+
function assertCapacity(name, value) {
|
|
53
|
+
if (!Number.isInteger(value) || value < 0) throw new Error(`[arc] ${name} must be a non-negative integer, got ${String(value)}.`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { evictToSize as n, touch as r, assertCapacity as t };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { i as compileAggRequest, t as adapterSupportsAggregate } from "./validate-
|
|
1
|
+
/*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as conjoinPolicyFilters } from "./filter-merge-IpGywvym.mjs";
|
|
3
|
+
import { C as toRepositoryFilter } from "./plugin-BccET1tL.mjs";
|
|
4
|
+
import { i as compileAggRequest, t as adapterSupportsAggregate } from "./validate-9yzHC1e2.mjs";
|
|
5
5
|
//#region src/core/aggregation/buildHandler.ts
|
|
6
6
|
/**
|
|
7
7
|
* Framework-agnostic aggregation execution. Runs safety guards,
|