@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.
- package/dist/index.d.mts +51 -0
- package/dist/index.mjs +55 -0
- package/package.json +48 -0
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|