@classytic/arc-involvement 0.1.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.
@@ -0,0 +1,51 @@
1
+ import { PermissionCheck } from "@classytic/arc/permissions";
2
+
3
+ //#region src/index.d.ts
4
+ interface InvolvementScopeOptions {
5
+ /** May this caller see ALL rows (HQ oversight)? Default: never. */
6
+ readonly isOversight?: (req: InvolvementScopeRequest) => boolean | Promise<boolean>;
7
+ /** Cast the org id for the filter (e.g. Mongo ObjectId). Default: identity. */
8
+ readonly castId?: (orgId: string) => unknown;
9
+ /**
10
+ * Org header name. MUST match the host's scope-resolution header
11
+ * (`createApp` scope `options.header`) — this filter is the visibility
12
+ * boundary, and a mismatched header would scope against a different org
13
+ * than the rest of the request pipeline. Default: `x-organization-id`
14
+ * (arc's scope default).
15
+ */
16
+ readonly header?: string;
17
+ }
18
+ interface InvolvementScopeRequest {
19
+ headers?: Record<string, unknown>;
20
+ scope?: {
21
+ organizationId?: string;
22
+ };
23
+ }
24
+ /**
25
+ * Involvement `$or` filter over `fields` for a party caller, or `null` when
26
+ * the caller has oversight (no scope — company-wide) or no org context.
27
+ *
28
+ * Header-first resolution, matching HQ-gate semantics (`x-organization-id`
29
+ * over session scope) so oversight checks and scoping agree on the org.
30
+ *
31
+ * Use in raw `/stats` / `/export` handlers:
32
+ * `const s = await resolveInvolvementScope(req, fields, opts); if (s) Object.assign(match, s);`
33
+ */
34
+ declare function resolveInvolvementScope(req: InvolvementScopeRequest, fields: readonly string[], options?: InvolvementScopeOptions): Promise<Record<string, unknown> | null>;
35
+ /**
36
+ * Wrap a resource's `list` `PermissionCheck`: run the base view gate, then
37
+ * RETURN the involvement scope as `filters`. Arc merges a PermissionResult's
38
+ * `filters` into `request._policyFilters`, which QueryResolver folds into the
39
+ * list query (`$or` supported). Returning the filter is the supported
40
+ * contract — mutating `request.metadata` is NOT read on the list path.
41
+ *
42
+ * ```ts
43
+ * list: involvementListScope(view, ['senderBranch', 'receiverBranch'], {
44
+ * isOversight: isHeadOffice,
45
+ * castId: (id) => new Types.ObjectId(id),
46
+ * }),
47
+ * ```
48
+ */
49
+ declare function involvementListScope(base: PermissionCheck, fields: readonly string[], options?: InvolvementScopeOptions): PermissionCheck;
50
+ //#endregion
51
+ export { InvolvementScopeOptions, InvolvementScopeRequest, involvementListScope, resolveInvolvementScope };
package/dist/index.mjs ADDED
@@ -0,0 +1,55 @@
1
+ //#region src/index.ts
2
+ /**
3
+ * Involvement `$or` filter over `fields` for a party caller, or `null` when
4
+ * the caller has oversight (no scope — company-wide) or no org context.
5
+ *
6
+ * Header-first resolution, matching HQ-gate semantics (`x-organization-id`
7
+ * over session scope) so oversight checks and scoping agree on the org.
8
+ *
9
+ * Use in raw `/stats` / `/export` handlers:
10
+ * `const s = await resolveInvolvementScope(req, fields, opts); if (s) Object.assign(match, s);`
11
+ */
12
+ async function resolveInvolvementScope(req, fields, options = {}) {
13
+ const headerName = options.header ?? "x-organization-id";
14
+ const orgId = req.headers?.[headerName] ?? req.scope?.organizationId;
15
+ if (!orgId) return null;
16
+ if (options.isOversight && await options.isOversight(req)) return null;
17
+ const id = options.castId ? options.castId(String(orgId)) : String(orgId);
18
+ return { $or: fields.map((f) => ({ [f]: id })) };
19
+ }
20
+ /**
21
+ * Wrap a resource's `list` `PermissionCheck`: run the base view gate, then
22
+ * RETURN the involvement scope as `filters`. Arc merges a PermissionResult's
23
+ * `filters` into `request._policyFilters`, which QueryResolver folds into the
24
+ * list query (`$or` supported). Returning the filter is the supported
25
+ * contract — mutating `request.metadata` is NOT read on the list path.
26
+ *
27
+ * ```ts
28
+ * list: involvementListScope(view, ['senderBranch', 'receiverBranch'], {
29
+ * isOversight: isHeadOffice,
30
+ * castId: (id) => new Types.ObjectId(id),
31
+ * }),
32
+ * ```
33
+ */
34
+ function involvementListScope(base, fields, options = {}) {
35
+ return async (ctx) => {
36
+ const result = await base(ctx);
37
+ if (!(typeof result === "boolean" ? result : result?.granted)) return result;
38
+ const scope = await resolveInvolvementScope(ctx.request, fields, options);
39
+ if (!scope) return result;
40
+ const baseFilters = typeof result === "object" && result !== null ? result.filters : void 0;
41
+ if (!baseFilters) return {
42
+ granted: true,
43
+ filters: scope
44
+ };
45
+ return {
46
+ granted: true,
47
+ filters: Object.keys(baseFilters).some((key) => key in scope) ? { $and: [baseFilters, scope] } : {
48
+ ...baseFilters,
49
+ ...scope
50
+ }
51
+ };
52
+ };
53
+ }
54
+ //#endregion
55
+ export { involvementListScope, resolveInvolvementScope };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@classytic/arc-involvement",
3
+ "version": "0.1.0",
4
+ "description": "Multi-party (involvement) visibility scoping for @classytic/arc — ERP-standard $or party filters with oversight bypass and base-permission filter composition",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.mts",
9
+ "default": "./dist/index.mjs"
10
+ }
11
+ },
12
+ "main": "./dist/index.mjs",
13
+ "types": "./dist/index.d.mts",
14
+ "sideEffects": false,
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "engines": {
22
+ "node": ">=22"
23
+ },
24
+ "scripts": {
25
+ "build": "tsdown",
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "biome check src/",
28
+ "prepublishOnly": "node ../../scripts/prepublish-gate.mjs"
29
+ },
30
+ "peerDependencies": {
31
+ "@classytic/arc": ">=2.20.0"
32
+ },
33
+ "keywords": [
34
+ "arc",
35
+ "fastify",
36
+ "multi-tenant",
37
+ "visibility",
38
+ "scoping",
39
+ "classytic"
40
+ ],
41
+ "license": "MIT",
42
+ "author": "Classytic",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/classytic/arc-ecosystem.git",
46
+ "directory": "packages/arc-involvement"
47
+ }
48
+ }