@classytic/arc 2.41.1 → 2.44.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-DSqD_Xsg.mjs → EventTransport-DZWDsaPa.mjs} +22 -17
- package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-yjEgn7Cm.mjs} +2 -2
- package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
- package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-DJxHUpE4.mjs} +44 -13
- package/dist/acquire-BozJ-9yt.mjs +35 -0
- package/dist/actionPermissions-5N04zBL4.mjs +772 -0
- package/dist/audit/index.d.mts +46 -8
- package/dist/audit/index.mjs +48 -26
- package/dist/auth/audit.d.mts +8 -8
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +46 -16
- package/dist/auth/index.mjs +134 -156
- package/dist/auth/redis-session.d.mts +4 -5
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-wX3SA9w_.mjs} +7 -102
- package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
- package/dist/boundedMap-DWiKW7lP.mjs +57 -0
- package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-Bswu_vNb.mjs} +4 -4
- package/dist/cache/index.d.mts +31 -43
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-DbCBxlgw.mjs → caching-Drp68DIS.mjs} +3 -3
- package/dist/claimed-DYvQoZLv.mjs +8 -0
- package/dist/cleanup/index.d.mts +18 -22
- package/dist/cleanup/index.mjs +27 -73
- package/dist/cli/commands/describe.d.mts +7 -5
- package/dist/cli/commands/describe.mjs +7 -4
- package/dist/cli/commands/docs.d.mts +2 -3
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.d.mts +2 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -3
- package/dist/cli/commands/init.mjs +2 -3
- package/dist/cli/commands/introspect.d.mts +2 -3
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-D5Ubn1fe.mjs → constants-B2QHb6u6.mjs} +4 -2
- package/dist/context/index.d.mts +11 -14
- 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-5lazhva9.mjs → core-COcqo0Cp.mjs} +15 -5
- package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-zG13rpPJ.mjs} +97 -39
- package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-CWRz96Vr.mjs} +11 -8
- package/dist/{preloadResources-f3boenVZ.mjs → createApp-BGLYtErt.mjs} +305 -394
- package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-DSztofa9.d.mts} +1 -1
- package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation--3xguLkk.mjs} +1 -1
- package/dist/{defineResource-DxkMTjRn.mjs → defineResource-gUp9ug66.mjs} +118 -1024
- package/dist/discovery/index.d.mts +6 -7
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +11 -5
- package/dist/docs/index.mjs +4 -4
- package/dist/{elevation-CkvG0LG2.mjs → elevation-BG4gnMsJ.mjs} +41 -12
- package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
- package/dist/encryption/index.d.mts +5 -5
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-COHw3N1b.mjs} +26 -5
- package/dist/{errors-BH7YU_9O.mjs → errors-CW4anJBy.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-zZY-unhr.mjs → eventPlugin-BweHAVws.mjs} +82 -21
- package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-N4-omQFR.mjs} +151 -17
- package/dist/events/index.d.mts +43 -18
- 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 +5 -6
- package/dist/events/transports/redis.mjs +2 -2
- package/dist/factory/index.d.mts +42 -27
- package/dist/factory/index.mjs +213 -32
- package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DRlCIWE9.d.mts} +11 -25
- package/dist/{fields-BHrkoF5y.mjs → fields-A5crhsJb.mjs} +1 -1
- package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
- package/dist/filter-merge-DGM-GQ1J.mjs +99 -0
- package/dist/{generate-BLRf2m3y.mjs → generate-BXKo2Gc8.mjs} +2 -2
- package/dist/{headers-BIXWKrzQ.mjs → headers-DKZQKL-Z.mjs} +2 -2
- package/dist/health-BYAwA6YS.mjs +22 -0
- package/dist/{health-ZRHeDHXH.mjs → health-CrqUp3sz.mjs} +115 -55
- package/dist/hooks/index.d.mts +2 -2
- package/dist/hooks/index.mjs +2 -3
- package/dist/idempotency/index.d.mts +36 -10
- package/dist/idempotency/index.mjs +230 -111
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +77 -12
- package/dist/{index-D4u76eQW.d.mts → index-Beg3aNnK.d.mts} +23 -101
- package/dist/{preloadResources-CjS2OfpU.d.mts → index-BkV55WlV.d.mts} +37 -38
- package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
- package/dist/index-CUJvoyw3.d.mts +510 -0
- package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
- package/dist/{index-DHbyPo5j.d.mts → index-WH7xgvxS.d.mts} +304 -254
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +11 -11
- package/dist/{init-CpLkjU18.mjs → init-BPzK5Lnr.mjs} +1 -1
- package/dist/integrations/event-gateway.d.mts +11 -7
- 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 +102 -89
- package/dist/integrations/mcp/index.mjs +19 -4
- package/dist/integrations/mcp/testing.d.mts +8 -9
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +85 -144
- package/dist/integrations/streamline.mjs +131 -55
- package/dist/integrations/webhooks.d.mts +83 -143
- package/dist/integrations/webhooks.mjs +36 -80
- package/dist/integrations/websocket-pushref-redis.d.mts +5 -6
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.d.mts +5 -6
- 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-JJzwH4Ro.d.mts} +34 -2
- package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-YIq-vGLv.mjs} +2 -2
- package/dist/{keys-cpZCtcIE.mjs → keys-CpoiZ8hU.mjs} +7 -18
- package/dist/{loadResources-BPfWiI5s.mjs → loadResources-6jO52-RR.mjs} +3 -3
- package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-MP8xEppu.mjs} +2 -2
- package/dist/logger/index.d.mts +9 -17
- package/dist/logger/index.mjs +2 -153
- package/dist/logger-D4V1zGLI.mjs +160 -0
- package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-srPc7ItN.mjs} +446 -170
- package/dist/{memory-Iotz0cCx.mjs → memory-CTa5pOCW.mjs} +36 -34
- package/dist/{metrics-NMsmpRmY.mjs → metrics-DsZrvnpB.mjs} +39 -72
- package/dist/middleware/index.d.mts +5 -5
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.d.mts +13 -13
- package/dist/migrations/index.mjs +2 -2
- package/dist/{module-DpLJG2_N.mjs → module--L0iBIZz.mjs} +8 -8
- package/dist/mongodb-memory-server-CHhYSq5y.mjs +35539 -0
- package/dist/mongoose-CYJCLyi-.mjs +67451 -0
- package/dist/multiTenant-CAtYlW6H.d.mts +104 -0
- package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-Cc7eZ9cq.mjs} +46 -7
- package/dist/node-D8UZv_8c.mjs +41236 -0
- package/dist/{openapi-CmFf3yq_.mjs → openapi-CMegfsm8.mjs} +48 -10
- package/dist/outbox-admin/index.d.mts +5 -5
- package/dist/outbox-admin/index.mjs +6 -18
- package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Ryk270jd.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-D8TBH44E.mjs → permissions-C5ijIz5Q.mjs} +149 -33
- package/dist/{pipe-FMWNheSO.mjs → pipe-D81zMtWs.mjs} +2 -2
- package/dist/pipeline/index.d.mts +7 -7
- package/dist/pipeline/index.mjs +2 -2
- package/dist/plan-CJGMKoui.mjs +565 -0
- package/dist/{BaseController-CYEwjyjN.mjs → plugin-DALd9dcF.mjs} +1269 -346
- package/dist/plugins/index.d.mts +17 -14
- package/dist/plugins/index.mjs +44 -22
- package/dist/plugins/response-cache.d.mts +5 -6
- package/dist/plugins/response-cache.mjs +8 -14
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-CXWZIzTv.mjs → pluralize-COUNYo3d.mjs} +1 -1
- package/dist/presets/filesUpload.d.mts +8 -8
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +35 -40
- 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 +9 -10
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-ZJSPgmT2.mjs → presets-CONf2Qem.mjs} +99 -107
- package/dist/{projection-BzIgzoKZ.mjs → projection-wFoLqdss.mjs} +2 -2
- package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
- package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-BATECpyh.mjs} +12 -14
- package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
- package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-DVEZ_UpV.mjs} +1 -1
- package/dist/{realtime-8tIQWZz4.mjs → realtime-xyubMKSB.mjs} +25 -21
- package/dist/{redis-Bzi1y3F9.d.mts → redis-BDgv9Glw.d.mts} +17 -24
- package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
- package/dist/redisShapes-CQksY67q.d.mts +55 -0
- package/dist/registry/index.d.mts +15 -10
- package/dist/registry/index.mjs +4 -4
- package/dist/{registry-DNkgGQcK.mjs → registry-Bdzg-hdm.mjs} +13 -10
- package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-BFJxGPuh.mjs} +1 -1
- package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-Bnc6lJyy.mjs} +2 -2
- package/dist/{requestContext-cDkAUr-P.mjs → requestContext-C10UeFe5.mjs} +1 -1
- package/dist/requirement-Bu50QALH.mjs +35 -0
- package/dist/requirement-CbiLdQ1n.d.mts +22 -0
- package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
- package/dist/resourceRateLimit-urWPJxrt.mjs +180 -0
- package/dist/rolldown-runtime-MYrLNGnq.mjs +45 -0
- package/dist/routeValidator-DHtpzrzk.mjs +70 -0
- package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-DIMKMV2G.mjs} +1 -1
- package/dist/{schedules-j6uXXAeJ.mjs → schedules-CKTs5ys8.mjs} +46 -37
- package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BVjkQ5DD.mjs} +2 -2
- package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-BA0Hb21Y.mjs} +2 -2
- package/dist/schemas/index.d.mts +20 -10
- package/dist/schemas/index.mjs +8 -3
- package/dist/scim/index.d.mts +13 -13
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.d.mts +4 -4
- package/dist/scope/index.mjs +6 -6
- package/dist/{scope-BUi1xV1I.mjs → scope-DINaj7Ax.mjs} +4 -4
- package/dist/shutdown-LpLMH8T1.mjs +59 -0
- package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-DbwIM1f1.mjs} +1 -1
- package/dist/{sse-BeqVKQKu.mjs → sse-BzJFPX_1.mjs} +11 -15
- package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-DQChzGTs.mjs} +2 -2
- package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-Coj9RhOZ.mjs} +1 -1
- package/dist/{streaming-IcRFjWQU.mjs → streaming-JzB5CAga.mjs} +1 -1
- package/dist/subscriberIdentity-DeyppIif.mjs +28 -0
- package/dist/tenancy-DxV8OuX2.mjs +43 -0
- package/dist/tenantPreparation-CBvvAcAj.mjs +67 -0
- package/dist/testing/cleanupStoreContract.d.mts +4 -5
- package/dist/testing/cleanupStoreContract.mjs +1 -1
- package/dist/testing/index.d.mts +215 -197
- package/dist/testing/index.mjs +289 -214
- package/dist/testing/outboxStoreContract.d.mts +4 -5
- package/dist/testing/outboxStoreContract.mjs +137 -2
- package/dist/testing/requiredPorts.d.mts +16 -0
- package/dist/testing/requiredPorts.mjs +65 -0
- package/dist/testing/storageContract.d.mts +4 -5
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/timing-CntavTlR.mjs +46 -0
- package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-orvnGu4F.mjs} +1 -1
- package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-kEaP_WmO.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-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
- package/dist/{types-CWQn9-IC.d.mts → types-D3Ixsh00.d.mts} +22 -9
- package/dist/{types-CgenB1gv.mjs → types-fvO6Ovzx.mjs} +4 -4
- package/dist/usage/index.d.mts +5 -6
- package/dist/usage/index.mjs +5 -5
- package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-C76zD5pa.mjs} +1 -1
- package/dist/utils/index.d.mts +125 -264
- package/dist/utils/index.mjs +13 -12
- package/dist/{utils-CxcctrAh.mjs → utils-BpkVZ8rB.mjs} +315 -404
- package/dist/{validate-DrYWnOzI.mjs → validate-CWp-lsrm.mjs} +6 -14
- package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
- package/dist/{versioning-DKkT4hNJ.mjs → versioning-CHhL9P3o.mjs} +4 -4
- package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
- package/dist/{workScope-CxKqoIFn.mjs → workScope-n9a7d_lZ.mjs} +2 -2
- package/package.json +57 -59
- 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-BoPGQ5-4.mjs +0 -350
- package/dist/index-CFfLKMH4.d.mts +0 -290
- package/dist/interface-YzIbF7Ii.mjs +0 -17
- package/dist/redis-stream-BPKscsTB.d.mts +0 -260
- package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
- package/dist/rolldown-runtime-eysyzFAF.mjs +0 -15
- 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.44.0 | MIT | https://classytic.com */
|
|
2
|
+
import { d as createDomainError, l as UnauthorizedError, p as isArcError, t as ArcError } from "../errors-CW4anJBy.mjs";
|
|
3
|
+
import { n as requireSingleHeaderValue } from "../headers-DKZQKL-Z.mjs";
|
|
4
|
+
import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-C76zD5pa.mjs";
|
|
5
|
+
import { o as toJsonSchema } from "../schemaConverter-BVjkQ5DD.mjs";
|
|
6
|
+
import { n as declareRuntimeCapability } from "../runtimeCapabilities-DIMKMV2G.mjs";
|
|
7
|
+
import { E as requireOrgRole, T as requireOrgMembership, k as requireTeamMembership } from "../permissions-C5ijIz5Q.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";
|
|
@@ -47,7 +48,10 @@ const authPlugin = async (fastify, opts = {}) => {
|
|
|
47
48
|
let jwtContext = null;
|
|
48
49
|
if (jwtConfig?.secret) {
|
|
49
50
|
if (jwtConfig.secret.length < 32) throw new Error(`JWT secret must be at least 32 characters (current: ${jwtConfig.secret.length}).\nUse a strong random secret for production.`);
|
|
50
|
-
const jwtPlugin = await import("@fastify/jwt")
|
|
51
|
+
const jwtPlugin = await import("@fastify/jwt").catch((error) => {
|
|
52
|
+
if (error instanceof Error && "code" in error && error.code === "ERR_MODULE_NOT_FOUND" && error.message.includes("@fastify/jwt")) throw new Error("Arc JWT authentication requires @fastify/jwt. Run `npm install @fastify/jwt`. Better Auth and custom authenticators do not require this package.", { cause: error });
|
|
53
|
+
throw error;
|
|
54
|
+
});
|
|
51
55
|
await fastify.register(jwtPlugin.default ?? jwtPlugin, {
|
|
52
56
|
secret: jwtConfig.secret,
|
|
53
57
|
sign: {
|
|
@@ -543,6 +547,22 @@ function mergeUserFieldsIntoRequestBody(requestBody, userFields, endpointPath) {
|
|
|
543
547
|
//#endregion
|
|
544
548
|
//#region src/auth/betterAuth.ts
|
|
545
549
|
/**
|
|
550
|
+
* Better Auth Adapter for Arc/Fastify
|
|
551
|
+
*
|
|
552
|
+
* Bridges Fastify <-> Better Auth's Fetch API (Request/Response).
|
|
553
|
+
* Better Auth is the USER's dependency -- Arc only provides this thin adapter.
|
|
554
|
+
*
|
|
555
|
+
* @example
|
|
556
|
+
* import { betterAuth } from 'better-auth';
|
|
557
|
+
* import { createBetterAuthAdapter } from '@classytic/arc/auth';
|
|
558
|
+
*
|
|
559
|
+
* const auth = betterAuth({ ... });
|
|
560
|
+
*
|
|
561
|
+
* const app = await createApp({
|
|
562
|
+
* auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth }) },
|
|
563
|
+
* });
|
|
564
|
+
*/
|
|
565
|
+
/**
|
|
546
566
|
* Convert a Fastify request into a Fetch API Request.
|
|
547
567
|
*
|
|
548
568
|
* Better Auth expects standard Web API Request objects.
|
|
@@ -604,28 +624,23 @@ async function sendFetchResponse(response, reply) {
|
|
|
604
624
|
}
|
|
605
625
|
}
|
|
606
626
|
/**
|
|
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.
|
|
627
|
+
* Resolve the current session via `auth.api.getSession`. Throws
|
|
628
|
+
* `ArcError(BETTER_AUTH_API_MISSING)` when it is absent (a stub, not a real
|
|
629
|
+
* `betterAuth()` instance).
|
|
612
630
|
*/
|
|
613
631
|
async function getSessionDirect(auth, headers) {
|
|
614
632
|
const api = auth.api;
|
|
615
|
-
if (!api || typeof api.getSession !== "function") throw new ArcError("Better Auth instance is missing `api.getSession` — arc
|
|
616
|
-
code:
|
|
633
|
+
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.", {
|
|
634
|
+
code: ARC_ERROR_CODES.AUTH_MISCONFIGURED,
|
|
617
635
|
statusCode: 500
|
|
618
636
|
});
|
|
619
637
|
const result = await api.getSession({ headers });
|
|
620
638
|
return result?.user ? result : null;
|
|
621
639
|
}
|
|
622
640
|
/**
|
|
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.
|
|
641
|
+
* Read a method from `auth.api`: the flat shape real instances expose
|
|
642
|
+
* (`api.getActiveMember`), falling back to the nested stub shape
|
|
643
|
+
* (`api.organization.getActiveMember`).
|
|
629
644
|
*/
|
|
630
645
|
function pickApiMethod(auth, name, group) {
|
|
631
646
|
const api = auth.api;
|
|
@@ -649,11 +664,8 @@ async function getActiveMemberRoles(auth, headers) {
|
|
|
649
664
|
}
|
|
650
665
|
}
|
|
651
666
|
/**
|
|
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.
|
|
667
|
+
* Resolve org roles for an explicit organizationId — for sessions without
|
|
668
|
+
* `activeOrganizationId` (e.g. API-key auth via `x-organization-id`).
|
|
657
669
|
*/
|
|
658
670
|
async function getMemberRolesByOrg(auth, headers, organizationId) {
|
|
659
671
|
const fn = pickApiMethod(auth, "getActiveMemberRole", "organization");
|
|
@@ -669,21 +681,12 @@ async function getMemberRolesByOrg(auth, headers, organizationId) {
|
|
|
669
681
|
}
|
|
670
682
|
}
|
|
671
683
|
/**
|
|
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.
|
|
684
|
+
* Teams the current user is a MEMBER of in one organization, used to validate
|
|
685
|
+
* `activeTeamId` before binding it to scope.
|
|
681
686
|
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
* user is not a member of — the organization check below constrains the org, not
|
|
686
|
-
* membership.
|
|
687
|
+
* Must stay `listUserTeams`: `listOrganizationTeams` lists EVERY team in the
|
|
688
|
+
* org and would admit a team the user isn't a member of. (`listTeams` is the
|
|
689
|
+
* client-side name; `auth.api` has no such method.)
|
|
687
690
|
*/
|
|
688
691
|
async function listTeamsDirect(auth, headers, organizationId) {
|
|
689
692
|
const fn = pickApiMethod(auth, "listUserTeams", "organization");
|
|
@@ -737,38 +740,72 @@ async function resolveActiveTeamId(input) {
|
|
|
737
740
|
if (!activeTeamId) return void 0;
|
|
738
741
|
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
742
|
}
|
|
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
|
-
*/
|
|
743
|
+
const AUTH_SCHEME = /^[A-Za-z][A-Za-z0-9!#$%&'*+.^_`|~-]*$/;
|
|
765
744
|
function createBetterAuthAdapter(options) {
|
|
766
|
-
const { auth, basePath = "/api/auth", orgContext: orgContextOpt = false, openapi: openapiOpt = true, userFields, exposeAuthErrors = false } = options;
|
|
745
|
+
const { auth, basePath = "/api/auth", orgContext: orgContextOpt = false, openapi: openapiOpt = true, userFields, exposeAuthErrors = false, resolveOrgRoles, memberLookup = "better-auth", credentials = {} } = options;
|
|
746
|
+
const verifiers = /* @__PURE__ */ new Map();
|
|
747
|
+
for (const [scheme, verify] of Object.entries(credentials)) {
|
|
748
|
+
if (!AUTH_SCHEME.test(scheme)) throw new Error(`createBetterAuthAdapter: "${scheme}" is not an auth scheme name.`);
|
|
749
|
+
if (scheme.toLowerCase() === "bearer") throw new Error("createBetterAuthAdapter: the Bearer scheme is the session's.");
|
|
750
|
+
verifiers.set(scheme.toLowerCase(), verify);
|
|
751
|
+
}
|
|
752
|
+
/** A registered non-session credential on this request, if any. */
|
|
753
|
+
const presentedCredential = (request) => {
|
|
754
|
+
if (verifiers.size === 0) return null;
|
|
755
|
+
const header = request.headers.authorization;
|
|
756
|
+
if (typeof header !== "string") return null;
|
|
757
|
+
const space = header.indexOf(" ");
|
|
758
|
+
if (space <= 0) return null;
|
|
759
|
+
const verify = verifiers.get(header.slice(0, space).toLowerCase());
|
|
760
|
+
return verify ? {
|
|
761
|
+
verify,
|
|
762
|
+
value: header.slice(space + 1).trim()
|
|
763
|
+
} : null;
|
|
764
|
+
};
|
|
767
765
|
const normalizedBase = basePath.replace(/\/+$/, "");
|
|
768
766
|
const orgEnabled = !!orgContextOpt;
|
|
767
|
+
if (resolveOrgRoles && !orgEnabled) throw new Error("createBetterAuthAdapter: `resolveOrgRoles` requires `orgContext: true`.");
|
|
768
|
+
if (memberLookup !== "better-auth" && memberLookup !== "resolver") throw new Error(`createBetterAuthAdapter: \`memberLookup\` must be 'better-auth' or 'resolver' (got ${String(memberLookup)}).`);
|
|
769
|
+
if (memberLookup === "resolver" && !resolveOrgRoles) throw new Error("createBetterAuthAdapter: `memberLookup: 'resolver'` requires `resolveOrgRoles` — without it nothing would read membership.");
|
|
770
|
+
const skipMemberLookup = memberLookup === "resolver";
|
|
769
771
|
/**
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
+
* The member scope for the active organization (session's active org wins; the header covers
|
|
773
|
+
* sessions without one), or `undefined` when the caller holds no role there. ONE path for
|
|
774
|
+
* `authenticate` and `optionalAuthenticate`, so `resolveOrgRoles` cannot apply on one and not the
|
|
775
|
+
* other.
|
|
776
|
+
*/
|
|
777
|
+
const resolveMemberScope = async (headers, session, requestedOrgId, userId, userRoles) => {
|
|
778
|
+
const activeOrgId = session?.activeOrganizationId || requestedOrgId;
|
|
779
|
+
if (!activeOrgId) return void 0;
|
|
780
|
+
let orgRoles = null;
|
|
781
|
+
if (!skipMemberLookup) {
|
|
782
|
+
if (session?.activeOrganizationId) orgRoles = await getActiveMemberRoles(auth, headers);
|
|
783
|
+
if (!orgRoles) orgRoles = await getMemberRolesByOrg(auth, headers, activeOrgId);
|
|
784
|
+
}
|
|
785
|
+
if (resolveOrgRoles && userId) orgRoles = await resolveOrgRoles({
|
|
786
|
+
userId,
|
|
787
|
+
organizationId: activeOrgId,
|
|
788
|
+
memberRoles: orgRoles
|
|
789
|
+
});
|
|
790
|
+
if (!orgRoles) return void 0;
|
|
791
|
+
const scope = {
|
|
792
|
+
kind: "member",
|
|
793
|
+
userId,
|
|
794
|
+
userRoles,
|
|
795
|
+
organizationId: activeOrgId,
|
|
796
|
+
orgRoles: [...orgRoles]
|
|
797
|
+
};
|
|
798
|
+
const activeTeamId = await resolveActiveTeamId({
|
|
799
|
+
auth,
|
|
800
|
+
headers,
|
|
801
|
+
session,
|
|
802
|
+
activeOrgId
|
|
803
|
+
});
|
|
804
|
+
if (activeTeamId) scope.teamId = activeTeamId;
|
|
805
|
+
return scope;
|
|
806
|
+
};
|
|
807
|
+
/**
|
|
808
|
+
* Validates the session via `auth.api.getSession` with the request headers.
|
|
772
809
|
*
|
|
773
810
|
* On success, sets `request.user` and `request.session`.
|
|
774
811
|
* When orgContext is enabled, also sets `request.scope` to
|
|
@@ -776,12 +813,26 @@ function createBetterAuthAdapter(options) {
|
|
|
776
813
|
* On failure, replies with 401.
|
|
777
814
|
*/
|
|
778
815
|
const authenticate = async (request, reply) => {
|
|
816
|
+
const presented = presentedCredential(request);
|
|
817
|
+
if (presented) {
|
|
818
|
+
const scope = await presented.verify(presented.value, request);
|
|
819
|
+
if (!scope) {
|
|
820
|
+
reply.code(401).send({
|
|
821
|
+
code: ARC_ERROR_CODES.UNAUTHORIZED,
|
|
822
|
+
message: "Invalid credentials",
|
|
823
|
+
status: 401
|
|
824
|
+
});
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
request.scope = scope;
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
779
830
|
try {
|
|
780
831
|
const headers = buildHeaders(request);
|
|
781
832
|
const sessionData = await getSessionDirect(auth, headers);
|
|
782
833
|
if (!sessionData?.user) {
|
|
783
834
|
reply.code(401).send({
|
|
784
|
-
code:
|
|
835
|
+
code: ARC_ERROR_CODES.UNAUTHORIZED,
|
|
785
836
|
message: "Invalid or expired session",
|
|
786
837
|
status: 401
|
|
787
838
|
});
|
|
@@ -799,47 +850,14 @@ function createBetterAuthAdapter(options) {
|
|
|
799
850
|
userRoles
|
|
800
851
|
};
|
|
801
852
|
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
|
-
}
|
|
853
|
+
const memberScope = await resolveMemberScope(headers, sessionData.session, requireSingleHeaderValue(request.headers, "x-organization-id"), userId, userRoles);
|
|
854
|
+
if (memberScope) req.scope = memberScope;
|
|
837
855
|
}
|
|
838
856
|
} catch (err) {
|
|
839
857
|
if (err instanceof ArcError) throw err;
|
|
840
858
|
const message = exposeAuthErrors ? err instanceof Error ? err.message : String(err) : "Authentication required";
|
|
841
859
|
reply.code(401).send({
|
|
842
|
-
code:
|
|
860
|
+
code: ARC_ERROR_CODES.UNAUTHORIZED,
|
|
843
861
|
message,
|
|
844
862
|
status: 401
|
|
845
863
|
});
|
|
@@ -854,6 +872,12 @@ function createBetterAuthAdapter(options) {
|
|
|
854
872
|
* flexible filter) can apply org-scoped queries when a user IS authenticated.
|
|
855
873
|
*/
|
|
856
874
|
const optionalAuthenticate = async (request, _reply) => {
|
|
875
|
+
const presented = presentedCredential(request);
|
|
876
|
+
if (presented) {
|
|
877
|
+
const scope = await presented.verify(presented.value, request);
|
|
878
|
+
if (scope) request.scope = scope;
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
857
881
|
try {
|
|
858
882
|
const headers = buildHeaders(request);
|
|
859
883
|
const sessionData = await getSessionDirect(auth, headers);
|
|
@@ -870,41 +894,8 @@ function createBetterAuthAdapter(options) {
|
|
|
870
894
|
userRoles: optUserRoles
|
|
871
895
|
};
|
|
872
896
|
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
|
-
}
|
|
897
|
+
const memberScope = await resolveMemberScope(headers, sessionData.session, requireSingleHeaderValue(request.headers, "x-organization-id"), optUserId, optUserRoles);
|
|
898
|
+
if (memberScope) req.scope = memberScope;
|
|
908
899
|
}
|
|
909
900
|
} catch (err) {
|
|
910
901
|
if (err instanceof ArcError) throw err;
|
|
@@ -914,22 +905,9 @@ function createBetterAuthAdapter(options) {
|
|
|
914
905
|
if (openapiOpt === false) extractedOpenApi = void 0;
|
|
915
906
|
else if (typeof openapiOpt === "object") extractedOpenApi = openapiOpt;
|
|
916
907
|
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
908
|
if (orgEnabled) {
|
|
931
909
|
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:
|
|
910
|
+
code: ARC_ERROR_CODES.AUTH_MISCONFIGURED,
|
|
933
911
|
statusCode: 500
|
|
934
912
|
});
|
|
935
913
|
}
|
|
@@ -940,7 +918,7 @@ function createBetterAuthAdapter(options) {
|
|
|
940
918
|
await sendFetchResponse(await auth.handler(fetchRequest), reply);
|
|
941
919
|
} catch (err) {
|
|
942
920
|
throw new ArcError("Authentication service error", {
|
|
943
|
-
code:
|
|
921
|
+
code: ARC_ERROR_CODES.AUTH_SERVICE_ERROR,
|
|
944
922
|
statusCode: 500,
|
|
945
923
|
cause: err instanceof Error ? err : new Error(String(err))
|
|
946
924
|
});
|
|
@@ -1403,4 +1381,4 @@ function mirrorTrustedOriginsFromCors(options) {
|
|
|
1403
1381
|
return Array.from(/* @__PURE__ */ new Set([canonicalUrl, ...corsOrigins]));
|
|
1404
1382
|
}
|
|
1405
1383
|
//#endregion
|
|
1406
|
-
export { MemorySessionStore, authPlugin_default as authPlugin,
|
|
1384
|
+
export { MemorySessionStore, authPlugin_default as authPlugin, createBetterAuthAdapter, createSessionManager, extractBetterAuthOpenApi, mirrorTrustedOriginsFromCors };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { i as SessionData, s as SessionStore } from "../sessionManager-qimlKV0P.mjs";
|
|
3
3
|
//#region src/auth/redis-session.d.ts
|
|
4
4
|
/** Minimal Redis client interface — compatible with ioredis */
|
|
5
|
-
interface RedisLike {
|
|
5
|
+
export interface RedisLike {
|
|
6
6
|
get(key: string): Promise<string | null>;
|
|
7
7
|
set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
|
|
8
8
|
del(...keys: string[]): Promise<number>;
|
|
@@ -11,7 +11,7 @@ interface RedisLike {
|
|
|
11
11
|
srem(key: string, ...members: string[]): Promise<number>;
|
|
12
12
|
expire(key: string, seconds: number): Promise<number>;
|
|
13
13
|
}
|
|
14
|
-
interface RedisSessionStoreOptions {
|
|
14
|
+
export interface RedisSessionStoreOptions {
|
|
15
15
|
/** Redis client instance (ioredis or compatible) */
|
|
16
16
|
redis: RedisLike;
|
|
17
17
|
/** Key prefix for session keys (default: 'arc:session:') */
|
|
@@ -28,7 +28,7 @@ interface RedisSessionStoreOptions {
|
|
|
28
28
|
*
|
|
29
29
|
* Session expiration is handled by Redis TTL — no cleanup interval needed.
|
|
30
30
|
*/
|
|
31
|
-
declare class RedisSessionStore implements SessionStore {
|
|
31
|
+
export declare class RedisSessionStore implements SessionStore {
|
|
32
32
|
private redis;
|
|
33
33
|
private prefix;
|
|
34
34
|
private userPrefix;
|
|
@@ -39,5 +39,4 @@ declare class RedisSessionStore implements SessionStore {
|
|
|
39
39
|
deleteAll(userId: string): Promise<void>;
|
|
40
40
|
deleteAllExcept(userId: string, currentSessionId: string): Promise<void>;
|
|
41
41
|
}
|
|
42
|
-
//#endregion
|
|
43
|
-
export { RedisLike, RedisSessionStore, RedisSessionStoreOptions };
|
|
42
|
+
//#endregion
|
|
@@ -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.44.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as arcLog } from "./logger-D4V1zGLI.mjs";
|
|
3
|
+
import { t as conjoinPolicyFilters } from "./filter-merge-DGM-GQ1J.mjs";
|
|
4
|
+
import { t as ArcError } from "./errors-CW4anJBy.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>;
|