@auth0/ai 6.0.2 → 6.0.3
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.
|
@@ -87,8 +87,37 @@ class FGAFilter {
|
|
|
87
87
|
}, new Map());
|
|
88
88
|
}
|
|
89
89
|
getCheckKey(check) {
|
|
90
|
-
|
|
90
|
+
// Include `context` and `contextualTuples` (both sent per check item in
|
|
91
|
+
// BatchCheck) so checks that differ only by either are treated as
|
|
92
|
+
// distinct and are neither incorrectly deduplicated nor correlated to the
|
|
93
|
+
// wrong batch-check result. See GHSA-rcmp-9f79-cfm9.
|
|
94
|
+
return `${check.user}|${check.object}|${check.relation}|${canonicalize(check.context)}|${canonicalize(check.contextualTuples)}`;
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
exports.FGAFilter = FGAFilter;
|
|
98
|
+
/**
|
|
99
|
+
* Produces a stable string for a `context` value: object keys are sorted at
|
|
100
|
+
* every depth so two equal contexts share a key regardless of key order, while
|
|
101
|
+
* any value difference — including deeply nested ones — yields a distinct key.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* `JSON.stringify(context, Object.keys(context).sort())` is NOT a safe
|
|
105
|
+
* substitute: its second argument is a property allowlist, not a sort order,
|
|
106
|
+
* and applied recursively it drops every nested key, collapsing distinct
|
|
107
|
+
* nested contexts to the same string. Array order is preserved because FGA
|
|
108
|
+
* `list` context values are order-sensitive.
|
|
109
|
+
*/
|
|
110
|
+
function canonicalize(value) {
|
|
111
|
+
if (value === null || typeof value !== "object") {
|
|
112
|
+
return JSON.stringify(value !== null && value !== void 0 ? value : null);
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(value)) {
|
|
115
|
+
return `[${value.map(canonicalize).join(",")}]`;
|
|
116
|
+
}
|
|
117
|
+
const obj = value;
|
|
118
|
+
return `{${Object.keys(obj)
|
|
119
|
+
.sort()
|
|
120
|
+
.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`)
|
|
121
|
+
.join(",")}}`;
|
|
122
|
+
}
|
|
94
123
|
//# sourceMappingURL=fga-filter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fga-filter.js","sourceRoot":"","sources":["../../../../src/authorizers/fga/fga-filter.ts"],"names":[],"mappings":";;;AAAA,sCAIsB;AAEtB,6CAAmE;AAWnE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAa,SAAS;IAKpB,YACE,EAAE,UAAU,EAAE,WAAW,EAAoB,EAC7C,eAAiC;QAEjC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,WAAW,IAAI,2BAAqB,CAAC,iBAAiB,CAAC;QAC1E,IAAI,CAAC,SAAS,GAAG,IAAA,+BAAkB,EAAC,eAAe,CAAC,CAAC;IACvD,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,MAAM,CACX,IAAsB,EACtB,eAAiC;QAEjC,OAAO,IAAI,SAAS,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;IAC9C,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,MAAM,CAAC,SAAc;QACzB,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,SAAS,CAAC,MAAM,CACnD,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACX,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YACzC,GAAG,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;YACxC,4DAA4D;YAC5D,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAClC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBAC7B,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC,EACD;YACE,MAAM,EAAE,EAA4B;YACpC,gBAAgB,EAAE,IAAI,GAAG,EAAa;YACtC,UAAU,EAAE,IAAI,GAAG,EAAU;SAC9B,CACF,CAAC;QAEF,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAE3D,OAAO,SAAS,CAAC,MAAM,CACrB,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAClE,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,gBAAgB,CAC5B,MAA8B;QAE9B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,CAC9C,EAAE,MAAM,EAAE,EACV;YACE,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B,CACF,CAAC;QAEF,OAAO,QAAQ,CAAC,MAAM,CAAC,MAAM,CAC3B,CAAC,aAAmC,EAAE,MAAM,EAAE,EAAE;YAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAClD,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,IAAI,KAAK,CAAC,CAAC;YACrD,OAAO,aAAa,CAAC;QACvB,CAAC,EACD,IAAI,GAAG,EAAmB,CAC3B,CAAC;IACJ,CAAC;IAEO,WAAW,CAAC,KAA2B;QAC7C,OAAO,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"fga-filter.js","sourceRoot":"","sources":["../../../../src/authorizers/fga/fga-filter.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ClientBatchCheckItem,\n ConsistencyPreference,\n OpenFgaClient,\n} from \"@openfga/sdk\";\n\nimport { buildOpenFgaClient, FGAClientParams } from \"./fga-client\";\n\nexport type FGAFilterCheckerFn<T extends Record<string, any>> = (\n doc: T\n) => ClientBatchCheckItem;\n\nexport type FGAFilterArgs<T extends Record<string, any>> = {\n buildQuery: FGAFilterCheckerFn<T>;\n consistency?: ConsistencyPreference;\n};\n\n/**\n * A filter that allows filtering documents based on access control checks\n * using OpenFGA. This class performs batch checks on passed documents,\n * returning only the ones that pass the specified access criteria.\n *\n * @remarks\n * The FGAFilter requires a buildQuery function to specify how access checks\n * are formed for each document, the checks are executed via an OpenFGA client\n * or equivalent mechanism. The checks are then mapped back to their corresponding\n * documents to filter out those for which access is denied.\n *\n * @example\n * ```ts\n * const filter = FGAFilter.create({\n * buildQuery: (doc) => ({\n * user: `user:${user}`,\n * object: `doc:${doc.metadata.id}`,\n * relation: \"viewer\",\n * }),\n * });\n * ```\n */\nexport class FGAFilter<T extends Record<string, any>> {\n private buildQuery: FGAFilterCheckerFn<T>;\n private consistency: ConsistencyPreference;\n private fgaClient: OpenFgaClient;\n\n private constructor(\n { buildQuery, consistency }: FGAFilterArgs<T>,\n fgaClientParams?: FGAClientParams\n ) {\n this.buildQuery = buildQuery;\n this.consistency = consistency || ConsistencyPreference.HigherConsistency;\n this.fgaClient = buildOpenFgaClient(fgaClientParams);\n }\n\n /**\n * Creates a new FGAFilter instance using the given arguments and optional OpenFgaClient.\n *\n * @param args - @FGAFilterArgs\n * @param args.buildQuery - A function to generate access check requests for each document.\n * @param args.consistency - Optional - The consistency preference for the OpenFGA client.\n * @param fgaClientParams - Optional - OpenFgaClient configuration to execute checks against.\n * @returns A newly created FGAFilter instance configured with the provided arguments.\n */\n static create<T extends Record<string, any>>(\n args: FGAFilterArgs<T>,\n fgaClientParams?: FGAClientParams\n ): FGAFilter<T> {\n return new FGAFilter(args, fgaClientParams);\n }\n\n /**\n * Filters documents based on the provided query parameters, processes\n * them through a checker function,\n * and filters the documents based on permissions.\n *\n * @param params - The query parameters used to retrieve nodes.\n * @returns A promise that resolves to an array of documents that have passed the permission checks.\n */\n async filter(documents: T[]): Promise<T[]> {\n const { checks, documentToObject } = documents.reduce(\n (acc, doc) => {\n const check = this.buildQuery(doc);\n const checkKey = this.getCheckKey(check);\n acc.documentToObject.set(doc, checkKey);\n // Skip duplicate checks for same user, object, and relation\n if (!acc.seenChecks.has(checkKey)) {\n acc.seenChecks.add(checkKey);\n acc.checks.push(check);\n }\n return acc;\n },\n {\n checks: [] as ClientBatchCheckItem[],\n documentToObject: new Map<T, string>(),\n seenChecks: new Set<string>(),\n }\n );\n\n const permissionsMap = await this.checkPermissions(checks);\n\n return documents.filter(\n (d) => permissionsMap.get(documentToObject.get(d) || \"\") === true\n );\n }\n\n /**\n * Checks permissions for a list of client requests.\n *\n * @param checks - An array of `ClientBatchCheckItem` objects representing the permissions to be checked.\n * @returns A promise that resolves to a `Map` where the keys are object identifiers and the values are booleans indicating whether the permission is allowed.\n */\n private async checkPermissions(\n checks: ClientBatchCheckItem[]\n ): Promise<Map<string, boolean>> {\n const response = await this.fgaClient.batchCheck(\n { checks },\n {\n consistency: this.consistency,\n }\n );\n\n return response.result.reduce(\n (permissionMap: Map<string, boolean>, result) => {\n const checkKey = this.getCheckKey(result.request);\n permissionMap.set(checkKey, result.allowed || false);\n return permissionMap;\n },\n new Map<string, boolean>()\n );\n }\n\n private getCheckKey(check: ClientBatchCheckItem): string {\n // Include `context` and `contextualTuples` (both sent per check item in\n // BatchCheck) so checks that differ only by either are treated as\n // distinct and are neither incorrectly deduplicated nor correlated to the\n // wrong batch-check result. See GHSA-rcmp-9f79-cfm9.\n return `${check.user}|${check.object}|${check.relation}|${canonicalize(\n check.context\n )}|${canonicalize(check.contextualTuples)}`;\n }\n}\n\n/**\n * Produces a stable string for a `context` value: object keys are sorted at\n * every depth so two equal contexts share a key regardless of key order, while\n * any value difference — including deeply nested ones — yields a distinct key.\n *\n * @remarks\n * `JSON.stringify(context, Object.keys(context).sort())` is NOT a safe\n * substitute: its second argument is a property allowlist, not a sort order,\n * and applied recursively it drops every nested key, collapsing distinct\n * nested contexts to the same string. Array order is preserved because FGA\n * `list` context values are order-sensitive.\n */\nfunction canonicalize(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value ?? null);\n }\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(\",\")}]`;\n }\n const obj = value as Record<string, unknown>;\n return `{${Object.keys(obj)\n .sort()\n .map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`)\n .join(\",\")}}`;\n}\n"]}
|
|
@@ -84,7 +84,36 @@ export class FGAFilter {
|
|
|
84
84
|
}, new Map());
|
|
85
85
|
}
|
|
86
86
|
getCheckKey(check) {
|
|
87
|
-
|
|
87
|
+
// Include `context` and `contextualTuples` (both sent per check item in
|
|
88
|
+
// BatchCheck) so checks that differ only by either are treated as
|
|
89
|
+
// distinct and are neither incorrectly deduplicated nor correlated to the
|
|
90
|
+
// wrong batch-check result. See GHSA-rcmp-9f79-cfm9.
|
|
91
|
+
return `${check.user}|${check.object}|${check.relation}|${canonicalize(check.context)}|${canonicalize(check.contextualTuples)}`;
|
|
88
92
|
}
|
|
89
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Produces a stable string for a `context` value: object keys are sorted at
|
|
96
|
+
* every depth so two equal contexts share a key regardless of key order, while
|
|
97
|
+
* any value difference — including deeply nested ones — yields a distinct key.
|
|
98
|
+
*
|
|
99
|
+
* @remarks
|
|
100
|
+
* `JSON.stringify(context, Object.keys(context).sort())` is NOT a safe
|
|
101
|
+
* substitute: its second argument is a property allowlist, not a sort order,
|
|
102
|
+
* and applied recursively it drops every nested key, collapsing distinct
|
|
103
|
+
* nested contexts to the same string. Array order is preserved because FGA
|
|
104
|
+
* `list` context values are order-sensitive.
|
|
105
|
+
*/
|
|
106
|
+
function canonicalize(value) {
|
|
107
|
+
if (value === null || typeof value !== "object") {
|
|
108
|
+
return JSON.stringify(value !== null && value !== void 0 ? value : null);
|
|
109
|
+
}
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
return `[${value.map(canonicalize).join(",")}]`;
|
|
112
|
+
}
|
|
113
|
+
const obj = value;
|
|
114
|
+
return `{${Object.keys(obj)
|
|
115
|
+
.sort()
|
|
116
|
+
.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`)
|
|
117
|
+
.join(",")}}`;
|
|
118
|
+
}
|
|
90
119
|
//# sourceMappingURL=fga-filter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fga-filter.js","sourceRoot":"","sources":["../../../../src/authorizers/fga/fga-filter.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,qBAAqB,GAEtB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,kBAAkB,EAAmB,MAAM,cAAc,CAAC;AAWnE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,OAAO,SAAS;IAKpB,YACE,EAAE,UAAU,EAAE,WAAW,EAAoB,EAC7C,eAAiC;QAEjC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,WAAW,IAAI,qBAAqB,CAAC,iBAAiB,CAAC;QAC1E,IAAI,CAAC,SAAS,GAAG,kBAAkB,CAAC,eAAe,CAAC,CAAC;IACvD,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,MAAM,CACX,IAAsB,EACtB,eAAiC;QAEjC,OAAO,IAAI,SAAS,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;IAC9C,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,MAAM,CAAC,SAAc;QACzB,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,GAAG,SAAS,CAAC,MAAM,CACnD,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACX,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YACzC,GAAG,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;YACxC,4DAA4D;YAC5D,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAClC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBAC7B,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC,EACD;YACE,MAAM,EAAE,EAA4B;YACpC,gBAAgB,EAAE,IAAI,GAAG,EAAa;YACtC,UAAU,EAAE,IAAI,GAAG,EAAU;SAC9B,CACF,CAAC;QAEF,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAE3D,OAAO,SAAS,CAAC,MAAM,CACrB,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,GAAG,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAClE,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,gBAAgB,CAC5B,MAA8B;QAE9B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,CAC9C,EAAE,MAAM,EAAE,EACV;YACE,WAAW,EAAE,IAAI,CAAC,WAAW;SAC9B,CACF,CAAC;QAEF,OAAO,QAAQ,CAAC,MAAM,CAAC,MAAM,CAC3B,CAAC,aAAmC,EAAE,MAAM,EAAE,EAAE;YAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YAClD,aAAa,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,OAAO,IAAI,KAAK,CAAC,CAAC;YACrD,OAAO,aAAa,CAAC;QACvB,CAAC,EACD,IAAI,GAAG,EAAmB,CAC3B,CAAC;IACJ,CAAC;IAEO,WAAW,CAAC,KAA2B;QAC7C,OAAO,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"fga-filter.js","sourceRoot":"","sources":["../../../../src/authorizers/fga/fga-filter.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ClientBatchCheckItem,\n ConsistencyPreference,\n OpenFgaClient,\n} from \"@openfga/sdk\";\n\nimport { buildOpenFgaClient, FGAClientParams } from \"./fga-client\";\n\nexport type FGAFilterCheckerFn<T extends Record<string, any>> = (\n doc: T\n) => ClientBatchCheckItem;\n\nexport type FGAFilterArgs<T extends Record<string, any>> = {\n buildQuery: FGAFilterCheckerFn<T>;\n consistency?: ConsistencyPreference;\n};\n\n/**\n * A filter that allows filtering documents based on access control checks\n * using OpenFGA. This class performs batch checks on passed documents,\n * returning only the ones that pass the specified access criteria.\n *\n * @remarks\n * The FGAFilter requires a buildQuery function to specify how access checks\n * are formed for each document, the checks are executed via an OpenFGA client\n * or equivalent mechanism. The checks are then mapped back to their corresponding\n * documents to filter out those for which access is denied.\n *\n * @example\n * ```ts\n * const filter = FGAFilter.create({\n * buildQuery: (doc) => ({\n * user: `user:${user}`,\n * object: `doc:${doc.metadata.id}`,\n * relation: \"viewer\",\n * }),\n * });\n * ```\n */\nexport class FGAFilter<T extends Record<string, any>> {\n private buildQuery: FGAFilterCheckerFn<T>;\n private consistency: ConsistencyPreference;\n private fgaClient: OpenFgaClient;\n\n private constructor(\n { buildQuery, consistency }: FGAFilterArgs<T>,\n fgaClientParams?: FGAClientParams\n ) {\n this.buildQuery = buildQuery;\n this.consistency = consistency || ConsistencyPreference.HigherConsistency;\n this.fgaClient = buildOpenFgaClient(fgaClientParams);\n }\n\n /**\n * Creates a new FGAFilter instance using the given arguments and optional OpenFgaClient.\n *\n * @param args - @FGAFilterArgs\n * @param args.buildQuery - A function to generate access check requests for each document.\n * @param args.consistency - Optional - The consistency preference for the OpenFGA client.\n * @param fgaClientParams - Optional - OpenFgaClient configuration to execute checks against.\n * @returns A newly created FGAFilter instance configured with the provided arguments.\n */\n static create<T extends Record<string, any>>(\n args: FGAFilterArgs<T>,\n fgaClientParams?: FGAClientParams\n ): FGAFilter<T> {\n return new FGAFilter(args, fgaClientParams);\n }\n\n /**\n * Filters documents based on the provided query parameters, processes\n * them through a checker function,\n * and filters the documents based on permissions.\n *\n * @param params - The query parameters used to retrieve nodes.\n * @returns A promise that resolves to an array of documents that have passed the permission checks.\n */\n async filter(documents: T[]): Promise<T[]> {\n const { checks, documentToObject } = documents.reduce(\n (acc, doc) => {\n const check = this.buildQuery(doc);\n const checkKey = this.getCheckKey(check);\n acc.documentToObject.set(doc, checkKey);\n // Skip duplicate checks for same user, object, and relation\n if (!acc.seenChecks.has(checkKey)) {\n acc.seenChecks.add(checkKey);\n acc.checks.push(check);\n }\n return acc;\n },\n {\n checks: [] as ClientBatchCheckItem[],\n documentToObject: new Map<T, string>(),\n seenChecks: new Set<string>(),\n }\n );\n\n const permissionsMap = await this.checkPermissions(checks);\n\n return documents.filter(\n (d) => permissionsMap.get(documentToObject.get(d) || \"\") === true\n );\n }\n\n /**\n * Checks permissions for a list of client requests.\n *\n * @param checks - An array of `ClientBatchCheckItem` objects representing the permissions to be checked.\n * @returns A promise that resolves to a `Map` where the keys are object identifiers and the values are booleans indicating whether the permission is allowed.\n */\n private async checkPermissions(\n checks: ClientBatchCheckItem[]\n ): Promise<Map<string, boolean>> {\n const response = await this.fgaClient.batchCheck(\n { checks },\n {\n consistency: this.consistency,\n }\n );\n\n return response.result.reduce(\n (permissionMap: Map<string, boolean>, result) => {\n const checkKey = this.getCheckKey(result.request);\n permissionMap.set(checkKey, result.allowed || false);\n return permissionMap;\n },\n new Map<string, boolean>()\n );\n }\n\n private getCheckKey(check: ClientBatchCheckItem): string {\n // Include `context` and `contextualTuples` (both sent per check item in\n // BatchCheck) so checks that differ only by either are treated as\n // distinct and are neither incorrectly deduplicated nor correlated to the\n // wrong batch-check result. See GHSA-rcmp-9f79-cfm9.\n return `${check.user}|${check.object}|${check.relation}|${canonicalize(\n check.context\n )}|${canonicalize(check.contextualTuples)}`;\n }\n}\n\n/**\n * Produces a stable string for a `context` value: object keys are sorted at\n * every depth so two equal contexts share a key regardless of key order, while\n * any value difference — including deeply nested ones — yields a distinct key.\n *\n * @remarks\n * `JSON.stringify(context, Object.keys(context).sort())` is NOT a safe\n * substitute: its second argument is a property allowlist, not a sort order,\n * and applied recursively it drops every nested key, collapsing distinct\n * nested contexts to the same string. Array order is preserved because FGA\n * `list` context values are order-sensitive.\n */\nfunction canonicalize(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value ?? null);\n }\n if (Array.isArray(value)) {\n return `[${value.map(canonicalize).join(\",\")}]`;\n }\n const obj = value as Record<string, unknown>;\n return `{${Object.keys(obj)\n .sort()\n .map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`)\n .join(\",\")}}`;\n}\n"]}
|